Want to monetize your Flutter app with subscriptions on both iOS and Android? This guide shows you how to add RevenueCat to Flutter, explains in_app_purchase plugin vs RevenueCat, and includes ready-to-use Flutter paywall code. I have shipped multiple apps with RevenueCat on Flutter, and this is the exact integration pattern I use every time.
Why RevenueCat Over the Raw in_app_purchase Plugin
Let me be direct: the official in_app_purchase Flutter plugin works. But "works" and "production-ready subscription infrastructure" are two very different things. Here are the specific pain points the raw plugin forces you to handle on your own:
- Server-side receipt validation — The plugin gives you purchase tokens on-device, but you still need a server to validate them with Apple and Google independently. That means building two separate validation endpoints, handling different payload formats, and managing certificate/key rotation.
- Cross-platform purchase unification — If a user subscribes on iOS and switches to Android, you need to sync that state yourself. The in_app_purchase plugin treats each store independently, so you need a backend to reconcile entitlements across platforms.
- Grace periods and billing retry — When a credit card fails, both Apple and Google retry billing. During that window, should the user keep access? The raw plugin gives you transaction state, but the logic for grace periods, retry states, and voluntary vs involuntary churn is entirely on you.
- Analytics and churn tracking — App Store Connect and Google Play Console give you basic subscriber counts, but no unified view, no cohort analysis, no MRR charts. If you want a single dashboard for both platforms, you are building that yourself.
- Price testing — Want to test $4.99/month vs $9.99/month across both stores? With the raw plugin, you create separate products on each platform, manage the logic yourself, and have no unified analytics. RevenueCat lets you do this from one dashboard.
I spent about 3 weeks building raw in_app_purchase infrastructure for my first Flutter app. For my second app, I switched to RevenueCat and had subscriptions working in an afternoon -- for both platforms. The difference is not close.
How RevenueCat Works with Flutter
This is important to understand: RevenueCat does not replace the native store APIs. It wraps them. When a user taps "Subscribe" in your app, here is what actually happens:
- Your Flutter widget calls
Purchases.purchasePackage(package) - RevenueCat's SDK calls the native store API (StoreKit on iOS, Google Play Billing on Android)
- The platform payment sheet appears -- this is 100% native, handled by the OS
- The user authenticates with Face ID, fingerprint, or password
- The store processes the payment and returns a signed transaction to the device
- RevenueCat intercepts the transaction and sends it to their servers for validation
- RevenueCat's servers verify the receipt, record the purchase, and return
CustomerInfo - Your app receives the updated
CustomerInfowith active entitlements
The purchase is always native. RevenueCat adds server-side validation, cross-platform sync, analytics, and subscription management on top. Your users never interact with RevenueCat directly.
in_app_purchase vs RevenueCat: The Full Comparison
| Feature | in_app_purchase (Raw) | RevenueCat |
|---|---|---|
| Server-side receipt validation | Manual (build for both stores) | Automatic |
| Cross-platform unification | Build your own backend | Automatic (iOS + Android + Web) |
| Analytics dashboard | Separate per platform | Unified MRR, churn, cohorts |
| A/B testing paywalls | Not supported | Built-in with Offerings |
| Grace period handling | Manual logic per platform | Automatic |
| Webhooks for events | Separate per store | Unified webhooks + integrations |
| Price | Free | Free up to $2.5K MTR |
| Setup time | 2-4 weeks (with server) | 1-2 hours |
| Sandbox testing | Manual per platform | Works with sandbox + debug logs |
The verdict: Use RevenueCat. You get native platform purchases with RevenueCat's unified subscription management, analytics, and server-side validation for both stores. That is exactly how The Flutter Kit is set up, and it is the pattern I recommend for every indie Flutter developer.
How to Add RevenueCat to Flutter -- Step by Step
Step 1: Install the SDK
Add the RevenueCat Flutter SDK to your pubspec.yaml:
# pubspec.yaml
dependencies:
purchases_flutter: ^8.0.0Run flutter pub get. That is it for the dependency. No separate iOS or Android package installs needed -- the Flutter plugin handles both platforms.
Step 2: Configure on App Launch
RevenueCat must be configured before any purchase-related calls. Initialize it in main.dartbefore runApp():
// main.dart
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:purchases_flutter/purchases_flutter.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Purchases.setLogLevel(LogLevel.debug); // Remove in production
PurchasesConfiguration configuration;
if (Platform.isIOS) {
configuration = PurchasesConfiguration('appl_YOUR_APPLE_KEY');
} else if (Platform.isAndroid) {
configuration = PurchasesConfiguration('goog_YOUR_GOOGLE_KEY');
} else {
throw UnsupportedError('Platform not supported');
}
await Purchases.configure(configuration);
runApp(const MyApp());
}Important: Use the correct platform-specific API key from the RevenueCat dashboard. Apple keys start with appl_ and Google keys start with goog_. This is the number one mistake I see.
Step 3: Create a PurchasesService
Wrapping RevenueCat in an abstract class makes your code testable and decoupled:
// purchases_service.dart
import 'package:purchases_flutter/purchases_flutter.dart';
abstract class PurchasesService {
Future<Offerings> fetchOfferings();
Future<CustomerInfo> purchase(Package package);
Future<CustomerInfo> restorePurchases();
Future<CustomerInfo> getCustomerInfo();
Future<bool> checkEntitlement(String id);
}
class RevenueCatPurchasesService implements PurchasesService {
@override
Future<Offerings> fetchOfferings() async {
final offerings = await Purchases.getOfferings();
if (offerings.current == null) {
throw PurchaseException('No offerings available');
}
return offerings;
}
@override
Future<CustomerInfo> purchase(Package package) async {
try {
final info = await Purchases.purchasePackage(package);
return info;
} on PlatformException catch (e) {
final errorCode = PurchasesErrorHelper.getErrorCode(e);
if (errorCode == PurchasesErrorCode.purchaseCancelledError) {
throw PurchaseException('Purchase was cancelled');
}
rethrow;
}
}
@override
Future<CustomerInfo> restorePurchases() async {
return await Purchases.restorePurchases();
}
@override
Future<CustomerInfo> getCustomerInfo() async {
return await Purchases.getCustomerInfo();
}
@override
Future<bool> checkEntitlement(String id) async {
try {
final info = await Purchases.getCustomerInfo();
return info.entitlements.all[id]?.isActive ?? false;
} catch (_) {
return false;
}
}
}
class PurchaseException implements Exception {
final String message;
PurchaseException(this.message);
@override
String toString() => message;
}Step 4: Build the Paywall Widget
Offerings are the products you configure in the RevenueCat dashboard. Each offering contains one or more packages (monthly, annual, lifetime). Here is a complete paywall widget:
// paywall_screen.dart
import 'package:flutter/material.dart';
import 'package:purchases_flutter/purchases_flutter.dart';
class PaywallScreen extends StatefulWidget {
const PaywallScreen({super.key});
@override
State<PaywallScreen> createState() => _PaywallScreenState();
}
class _PaywallScreenState extends State<PaywallScreen> {
Offerings? _offerings;
Package? _selectedPackage;
bool _isPurchasing = false;
String? _errorMessage;
@override
void initState() {
super.initState();
_loadOfferings();
}
Future<void> _loadOfferings() async {
try {
final offerings = await Purchases.getOfferings();
setState(() {
_offerings = offerings;
_selectedPackage =
offerings.current?.availablePackages.firstOrNull;
});
} catch (e) {
setState(() => _errorMessage = e.toString());
}
}
Future<void> _handlePurchase() async {
if (_selectedPackage == null) return;
setState(() {
_isPurchasing = true;
_errorMessage = null;
});
try {
final info =
await Purchases.purchasePackage(_selectedPackage!);
if (info.entitlements.all['premium']?.isActive ?? false) {
if (mounted) Navigator.of(context).pop(true);
}
} on PlatformException catch (e) {
final code = PurchasesErrorHelper.getErrorCode(e);
if (code != PurchasesErrorCode.purchaseCancelledError) {
setState(() => _errorMessage = e.message);
}
}
setState(() => _isPurchasing = false);
}
Future<void> _handleRestore() async {
try {
final info = await Purchases.restorePurchases();
if (info.entitlements.all['premium']?.isActive ?? false) {
if (mounted) Navigator.of(context).pop(true);
} else {
setState(() =>
_errorMessage = 'No active subscription found.');
}
} catch (e) {
setState(() => _errorMessage = e.toString());
}
}
@override
Widget build(BuildContext context) {
final packages =
_offerings?.current?.availablePackages ?? [];
return Scaffold(
appBar: AppBar(title: const Text('Upgrade to Pro')),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
children: [
if (packages.isEmpty)
const Center(child: CircularProgressIndicator())
else
...packages.map((pkg) => _PackageCard(
package: pkg,
isSelected: _selectedPackage == pkg,
onTap: () =>
setState(() => _selectedPackage = pkg),
)),
const SizedBox(height: 16),
if (_errorMessage != null)
Text(_errorMessage!,
style: const TextStyle(color: Colors.red)),
const Spacer(),
FilledButton(
onPressed: _isPurchasing ? null : _handlePurchase,
child: _isPurchasing
? const CircularProgressIndicator()
: const Text('Subscribe Now'),
),
TextButton(
onPressed: _handleRestore,
child: const Text('Restore Purchases'),
),
],
),
),
);
}
}Step 5: Gate Premium Features with Entitlements
Checking entitlements controls access to premium features. I recommend a ChangeNotifier that the rest of your app can listen to:
// premium_manager.dart
import 'package:flutter/material.dart';
import 'package:purchases_flutter/purchases_flutter.dart';
class PremiumManager extends ChangeNotifier {
bool _isPro = false;
bool get isPro => _isPro;
Future<void> checkStatus() async {
try {
final info = await Purchases.getCustomerInfo();
_isPro = info.entitlements.all['premium']?.isActive ?? false;
notifyListeners();
} catch (_) {
_isPro = false;
notifyListeners();
}
}
}
// Usage in any widget:
class FeatureScreen extends StatelessWidget {
const FeatureScreen({super.key});
@override
Widget build(BuildContext context) {
final premium = context.watch<PremiumManager>();
if (premium.isPro) {
return const PremiumContent();
}
return const LockedContent();
}
}Step 6: Listen for Subscription Changes
Subscriptions can change at any time. RevenueCat provides a listener for real-time updates:
// In your main.dart or app initialization
Purchases.addCustomerInfoUpdateListener((info) {
final isPro =
info.entitlements.all['premium']?.isActive ?? false;
// Update your PremiumManager or BLoC
getIt<PremiumManager>().updateStatus(isPro);
});Platform-Specific Setup
iOS Setup
- In App Store Connect, create your subscription products (monthly, annual, etc.)
- In the RevenueCat dashboard, create an Apple app and add the products to an Offering
- Set the App Store Connect Shared Secret in RevenueCat
- Configure App Store Server Notifications URL (provided by RevenueCat)
Android Setup
- In Google Play Console, create subscription products
- In RevenueCat, create a Google Play app and add the products
- Upload your Google Play service account JSON to RevenueCat
- Configure Real-time Developer Notifications (RTDN) topic
Sandbox Testing Guide
Never test purchases with real money during development. Both platforms provide sandbox environments:
- iOS Sandbox — Create a Sandbox Apple ID in App Store Connect. Subscriptions auto-renew every 5 minutes (monthly) and expire after 6 renewals.
- Android Test — Add license testers in Google Play Console. Test purchases are free and auto-renew on an accelerated schedule.
- RevenueCat Debug Mode — Use
Purchases.setLogLevel(LogLevel.debug)to see every API call in the Flutter console. - Check the RevenueCat Dashboard — Search for your sandbox user to see the full transaction history and entitlements.
Common Mistakes to Avoid
- Using the wrong API key per platform — RevenueCat generates separate keys for Apple and Google. If you use the Apple key on Android, nothing works.
- Not handling purchase cancellation — The user tapping "Cancel" is not an error. Catch
PurchasesErrorCode.purchaseCancelledErrorand handle it silently. - Forgetting the Restore Purchases button — Apple requires this for App Store approval. Include it on the paywall and in the settings screen.
- Not setting up server notifications — Without App Store Server Notifications and Google RTDN, RevenueCat cannot receive real-time subscription updates.
- Hardcoding product IDs — Use Offerings from RevenueCat instead. This lets you change products without shipping an app update.
- Not testing on real devices — Emulator purchase testing has limitations. Always test on physical devices before submitting.
Production Checklist
- Remove debug logging in production builds
- Verify API keys are correct per platform
- Test all purchase flows in sandbox (iOS) and test mode (Android)
- Ensure Restore Purchases button exists and works
- Handle the "no offerings" state gracefully
- Verify entitlement names match the RevenueCat dashboard
- Set up App Store Server Notifications and Google RTDN
- Test on real devices for both platforms
- Verify paywall shows correct localized prices
- Check that paywall design meets store guidelines
Skip the Manual Setup -- Use The Flutter Kit
Everything I covered above — the PurchasesService abstraction, the paywall widget, entitlement checking, restore purchases, subscription listeners, error handling — is already built and tested in The Flutter Kit. You get pre-built paywall templates, a complete subscription management layer, trial flow handling, and a production-ready architecture for both iOS and Android. Just paste your RevenueCat API keys in the config file and you are shipping.
Check out the full feature list or get started today for $69. If you are serious about monetizing your Flutter app, do not waste weeks on subscription infrastructure. Build your actual product instead.