RevenueCat handles the complexity of in-app subscriptions across iOS and Android. You configure products in the app stores, set up RevenueCat's dashboard to organize them, then use a few SDK calls to manage the entire purchase flow in your Flutter app.
For the full reference, see the RevenueCat Flutter docs.
Dashboard Setup
Before writing any code, you need to configure three things in the stores and in RevenueCat. Here is the flow:
- Create products in the stores. In App Store Connect (iOS) or Google Play Console (Android), create your subscription products with pricing and duration (monthly, yearly, etc.).
- Create a RevenueCat project. Sign up at revenuecat.com (free tier available), create a project, and connect your App Store and/or Play Store credentials.
- Create an Entitlement.An entitlement is a feature your users unlock (e.g., "premium"). This is what your app checks to decide if a user has access.
- Create Products. In RevenueCat, add your store product IDs and attach them to the entitlement.
- Create an Offering.An offering groups products into packages (monthly, annual, etc.) that you display in your paywall. You can change what's in an offering without updating your app.
Initialize
Add the RevenueCat package to your pubspec.yaml:
dependencies:
purchases_flutter: ^7.0.0Configure it in your main() function. Use platform- specific API keys (found in your RevenueCat dashboard under API Keys).
import 'dart:io';
import 'package:purchases_flutter/purchases_flutter.dart';
Future<void> initRevenueCat() async {
final apiKey = Platform.isIOS
? 'appl_your_ios_key'
: 'goog_your_android_key';
await Purchases.configure(
PurchasesConfiguration(apiKey),
);
}Fetch Offerings
Offerings represent what you show on your paywall. Each offering contains one or more packages (monthly, annual, lifetime, etc.). Fetch them from RevenueCat to display products with their store-localized titles and prices.
Future<List<Package>> getPackages() async {
final offerings = await Purchases.getOfferings();
final current = offerings.current;
if (current == null) return [];
return current.availablePackages;
}
// Each package gives you store-localized info:
// package.storeProduct.title — "Premium Monthly"
// package.storeProduct.description — "Unlock all features"
// package.storeProduct.priceString — "$4.99"
// package.packageType — monthly, annual, lifetime, etc.Make a Purchase
Call purchasePackage with the package the user selected. RevenueCat handles the native purchase dialog, receipt validation, and entitlement activation. Check the result to see if the entitlement is now active.
Future<bool> purchase(Package package) async {
try {
final result = await Purchases.purchasePackage(package);
// Check if the user now has premium access
return result.entitlements.active.containsKey('premium');
} on PlatformException catch (e) {
final errorCode = PurchasesErrorHelper.getErrorCode(e);
if (errorCode == PurchasesErrorCode.purchaseCancelledError) {
return false; // user cancelled, not an error
}
rethrow;
}
}Check Entitlements
An entitlement represents access to a feature. When you want to know if the user is subscribed, check if their entitlement is active. This is the single source of truth, and it works the same regardless of which store the purchase came from.
Future<bool> isPremium() async {
final customerInfo = await Purchases.getCustomerInfo();
return customerInfo.entitlements.active.containsKey('premium');
}You can also listen for real-time changes (e.g., when a subscription renews or expires):
Purchases.addCustomerInfoUpdateListener((customerInfo) {
final isPremium = customerInfo.entitlements.active.containsKey('premium');
// Update your app state
});Restore Purchases
Apple requires a restore button in your app. This recovers purchases when a user reinstalls, switches devices, or resets their phone.
Future<bool> restorePurchases() async {
final customerInfo = await Purchases.restorePurchases();
return customerInfo.entitlements.active.containsKey('premium');
}Identify Users
If your app has its own auth system (e.g., Supabase Auth), link RevenueCat to your user IDs so purchases follow the user across devices and platforms.
// After your user logs in
await Purchases.logIn('your-user-id');
// After your user logs out
await Purchases.logOut();Paywall Pattern
A complete paywall screen that fetches offerings and lets the user pick a plan:
class PaywallScreen extends StatefulWidget {
const PaywallScreen({super.key});
@override
State<PaywallScreen> createState() => _PaywallScreenState();
}
class _PaywallScreenState extends State<PaywallScreen> {
List<Package> packages = [];
bool loading = true;
@override
void initState() {
super.initState();
loadOfferings();
}
Future<void> loadOfferings() async {
final offerings = await Purchases.getOfferings();
setState(() {
packages = offerings.current?.availablePackages ?? [];
loading = false;
});
}
@override
Widget build(BuildContext context) {
if (loading) return Center(child: CircularProgressIndicator());
return ListView.builder(
itemCount: packages.length,
itemBuilder: (context, index) {
final package = packages[index];
final product = package.storeProduct;
return ListTile(
title: Text(product.title),
subtitle: Text(product.description),
trailing: Text(product.priceString),
onTap: () async {
try {
final result = await Purchases.purchasePackage(package);
if (result.entitlements.active.containsKey('premium')) {
Navigator.pop(context, true);
}
} on PlatformException {
// Handle error
}
},
);
},
);
}
}