Monetization: IAP, Subscriptions, Ads & RevenueCat
Implementing in-app purchases, subscription lifecycles, and ad monetization the right way
Open interactive version (quiz + challenge)Real-world analogy
Monetization is like running a coffee shop: you can sell individual drinks (one-time IAP), sell monthly subscriptions (premium membership), or let advertisers put posters on your wall (ads). RevenueCat is like a POS system that handles all payment types, tracks memberships, and talks to the credit card networks so you don't have to.
What is it?
Monetization in Flutter apps involves implementing in-app purchases, subscriptions, and advertising using platform-native billing systems abstracted by packages like in_app_purchase and RevenueCat, combined with ad SDKs like AdMob.
Real-world relevance
A field operations SaaS app uses RevenueCat to manage team subscriptions: free tier for 3 users, Pro at 29 USD/month for unlimited users. RevenueCat handles Apple/Google receipt validation, grace periods when a company's card expires, and sends webhooks to the NestJS backend to update the team's feature access in Supabase.
Key points
- In-app purchase types — Consumables (coins, credits — can be purchased repeatedly), Non-consumables (remove ads, unlock level — bought once), Auto-renewable subscriptions (monthly/annual — renews until cancelled), Non-renewing subscriptions (time-limited access, not auto-renewed). Each has different restoration logic.
- StoreKit (iOS) and Play Billing (Android) — iOS uses StoreKit 2 (modern Swift API, introduced iOS 15) and the legacy StoreKit 1. Android uses Google Play Billing Library 6+. Both have different product setup flows, testing environments (StoreKit Testing/TestFlight vs license testers), and receipt formats.
- in_app_purchase package — Flutter's official package:in_app_purchase abstracts StoreKit and Play Billing behind a common API. Handles: product listing, purchase initiation, purchase stream, delivery verification, and restore. Lower-level but cross-platform without a third-party dependency.
- RevenueCat integration — RevenueCat is a cross-platform subscription management SDK. It handles purchase validation, webhook delivery, entitlement management, and analytics. purchases_flutter is the Flutter SDK. Reduces 6 weeks of billing code to 2 days. Used by thousands of apps including major fintech products.
- Entitlements and offerings — RevenueCat models access as Entitlements (what the user can access: 'premium') mapped to Products (App Store/Play Store SKUs). Offerings are paywalls — different product bundles you can A/B test. Change products without app release by configuring in RevenueCat dashboard.
- Subscription lifecycle — Trial → Active → Grace period (payment failed, access continues briefly) → Lapsed (access revoked) → Cancelled → Expired. RevenueCat tracks all states via CustomerInfo. Your app checks entitlements, not raw subscription state, for cleaner logic.
- Receipt validation and server-side verification — Apple/Google receipts must be validated server-side to prevent fraud. RevenueCat handles this automatically. For custom backends: POST the receipt to Apple/Google validation servers, check the response, and update your DB. Never trust client-side receipt data alone.
- Paywall strategies — Hard paywalls (block feature entirely), soft paywalls (show feature, gate on use), freemium (core free, power features paid), trial-first (14-day trial before payment). RevenueCat Paywalls allows building no-code paywalls with remote configuration and A/B testing.
- AdMob integration — google_mobile_ads package provides Banner, Interstitial, Rewarded, and Native ads. Always initialize before using: MobileAds.instance.initialize(). Load ads before showing — preload rewarded ads especially. Comply with ATT (iOS) and consent requirements (GDPR) or ads won't show.
- ATT and consent frameworks — iOS 14+ requires App Tracking Transparency prompt before showing personalized ads. Use app_tracking_transparency package. Android requires UMP (User Messaging Platform) consent for GDPR. Failure to implement costs significant ad revenue in EU/US markets.
- Hybrid monetization — Many successful apps combine subscriptions + IAP + ads: free tier shows ads, paid subscription removes ads, consumable IAP for power users. Design your entitlement model to handle all three. RevenueCat's CustomerInfo.activeSubscriptions + activeEntitlements covers subscription state; ad visibility logic is separate.
- Pricing and conversion optimization — Offer annual plan prominently (typically 30-50% discount vs monthly — higher LTV). Trial conversion rates: 7-day trial converts better than 14-day in many categories. Price anchoring: show annual/monthly/weekly options to make monthly look reasonable. Test with RevenueCat Experiments.
Code example
// RevenueCat Flutter integration
import 'package:purchases_flutter/purchases_flutter.dart';
class PurchaseService {
static Future<void> initialize() async {
await Purchases.setLogLevel(LogLevel.debug);
final config = PurchasesConfiguration(
Platform.isIOS ? 'appl_your_key' : 'goog_your_key',
);
await Purchases.configure(config);
}
// Identify user after login (links purchases to your user ID)
static Future<void> identifyUser(String userId) async {
await Purchases.logIn(userId);
}
// Check if user has 'premium' entitlement
static Future<bool> hasPremiumAccess() async {
final customerInfo = await Purchases.getCustomerInfo();
return customerInfo.entitlements.active.containsKey('premium');
}
// Fetch current offering and display paywall
static Future<Offerings> getOfferings() async {
return await Purchases.getOfferings();
}
// Purchase a package (e.g., monthly subscription)
static Future<CustomerInfo> purchasePackage(Package package) async {
try {
return await Purchases.purchasePackage(package);
} on PurchasesErrorCode catch (e) {
if (e == PurchasesErrorCode.purchaseCancelledError) {
throw const UserCancelledPurchaseException();
}
rethrow;
}
}
// Restore purchases (required by App Store guidelines)
static Future<CustomerInfo> restorePurchases() async {
return await Purchases.restorePurchases();
}
}
// Usage in paywall widget
class PaywallScreen extends StatefulWidget {
const PaywallScreen({super.key});
@override
State<PaywallScreen> createState() => _PaywallScreenState();
}
class _PaywallScreenState extends State<PaywallScreen> {
Offerings? _offerings;
@override
void initState() {
super.initState();
PurchaseService.getOfferings().then((o) => setState(() => _offerings = o));
}
@override
Widget build(BuildContext context) {
final offering = _offerings?.current;
if (offering == null) return const CircularProgressIndicator();
return Column(
children: offering.availablePackages.map((pkg) {
return ElevatedButton(
onPressed: () async {
final info = await PurchaseService.purchasePackage(pkg);
if (info.entitlements.active.containsKey('premium')) {
Navigator.pop(context, true);
}
},
child: Text('${pkg.packageType.name} — ${pkg.storeProduct.priceString}'),
);
}).toList(),
);
}
}Line-by-line walkthrough
- 1. PurchasesConfiguration takes the platform-specific key — using Platform.isIOS ternary avoids shipping both keys to both platforms unnecessarily.
- 2. Purchases.logIn(userId) must be called after your own auth completes — it links anonymous device purchases to your authenticated user, enabling cross-device subscription access.
- 3. entitlements.active.containsKey('premium') is the canonical access check — always check entitlements, never raw subscription state, for clean feature gating logic.
- 4. Purchases.getOfferings() fetches the current offering configured in RevenueCat dashboard — changing products requires no app update.
- 5. offering.availablePackages contains the packages defined for that offering — monthly, annual, weekly, or custom configured in the dashboard.
- 6. purchasePackage wraps the native StoreKit/Play Billing purchase flow — RevenueCat handles receipt posting and validation automatically.
- 7. PurchasesErrorCode.purchaseCancelledError must be caught and handled gracefully — it's a normal user action, not an error to report.
- 8. Purchases.restorePurchases() is required by App Store Review guidelines — failure to include it is a common rejection reason.
Spot the bug
class PremiumGate extends StatelessWidget {
const PremiumGate({super.key, required this.child});
final Widget child;
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: Purchases.getCustomerInfo(),
builder: (context, snapshot) {
final info = snapshot.data;
final isPremium = info?.activeSubscriptions.isNotEmpty ?? false;
if (!isPremium) return const PaywallScreen();
return child;
},
);
}
}Need a hint?
A user buys a one-time non-consumable 'lifetime access' IAP. PremiumGate still shows the paywall. Why?
Show answer
Bug: activeSubscriptions only contains auto-renewable subscriptions — it does NOT include non-consumable or non-renewing purchases. A lifetime IAP would be delivered as a non-consumable, appearing in entitlements.active or nonSubscriptionTransactions, not activeSubscriptions. Fix: check entitlements instead — info?.entitlements.active.containsKey('premium') ?? false — this is the correct check because RevenueCat entitlements aggregate all access-granting purchases (subscriptions, non-consumables, promotional grants) into a single boolean. Also, FutureBuilder recreates the Future on every rebuild — cache getCustomerInfo() result or use a StreamBuilder with Purchases.customerInfoStream for reactive updates.
Explain like I'm 5
Making money from your app is like running a lemonade stand with different options: sell a glass (one-time purchase), sell a monthly membership card (subscription), or let someone put a sign near your stand for money (ads). RevenueCat is your cashier who handles all the payments, remembers who has a membership card, and makes sure no one sneaks past without paying.
Fun fact
RevenueCat processes over 1 billion USD in annual in-app revenue across its platform and is used by apps in over 30,000 companies. Its SDK typically saves 4-8 weeks of engineering time per app compared to implementing StoreKit and Play Billing directly.
Hands-on challenge
Implement a complete paywall flow: (1) Initialize RevenueCat on app start with platform-specific API keys. (2) After login, identify the user. (3) Build a PaywallScreen that fetches the current offering and displays monthly/annual packages with prices. (4) Handle the purchase flow including cancellation and error states. (5) Gate a premium feature behind an entitlement check. (6) Add a Restore Purchases button.
More resources
- RevenueCat Flutter SDK documentation (RevenueCat Docs)
- purchases_flutter package (pub.dev)
- Flutter in_app_purchase package (pub.dev)
- Google Mobile Ads Flutter (pub.dev)
- Subscription lifecycle states explained (RevenueCat Blog)