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:

  1. 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.).
  2. 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.
  3. 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.
  4. Create Products. In RevenueCat, add your store product IDs and attach them to the entitlement.
  5. 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:

yaml
dependencies:
  purchases_flutter: ^7.0.0

Configure it in your main() function. Use platform- specific API keys (found in your RevenueCat dashboard under API Keys).

dart
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.

dart
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.

dart
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.

dart
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):

dart
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.

dart
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.

dart
// 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:

dart
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
            }
          },
        );
      },
    );
  }
}