The Flutter Kit logoThe Flutter Kit
Tutorial

How to Add RevenueCat to Flutter: Step-by-Step Subscription Tutorial

Step-by-step guide to integrating RevenueCat subscriptions in your Flutter app for both iOS and Android.

Ahmed GaganAhmed Gagan
14 min read

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:

  1. Your Flutter widget calls Purchases.purchasePackage(package)
  2. RevenueCat's SDK calls the native store API (StoreKit on iOS, Google Play Billing on Android)
  3. The platform payment sheet appears -- this is 100% native, handled by the OS
  4. The user authenticates with Face ID, fingerprint, or password
  5. The store processes the payment and returns a signed transaction to the device
  6. RevenueCat intercepts the transaction and sends it to their servers for validation
  7. RevenueCat's servers verify the receipt, record the purchase, and return CustomerInfo
  8. Your app receives the updated CustomerInfo with 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

Featurein_app_purchase (Raw)RevenueCat
Server-side receipt validationManual (build for both stores)Automatic
Cross-platform unificationBuild your own backendAutomatic (iOS + Android + Web)
Analytics dashboardSeparate per platformUnified MRR, churn, cohorts
A/B testing paywallsNot supportedBuilt-in with Offerings
Grace period handlingManual logic per platformAutomatic
Webhooks for eventsSeparate per storeUnified webhooks + integrations
PriceFreeFree up to $2.5K MTR
Setup time2-4 weeks (with server)1-2 hours
Sandbox testingManual per platformWorks 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.0

Run 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

  1. In App Store Connect, create your subscription products (monthly, annual, etc.)
  2. In the RevenueCat dashboard, create an Apple app and add the products to an Offering
  3. Set the App Store Connect Shared Secret in RevenueCat
  4. Configure App Store Server Notifications URL (provided by RevenueCat)

Android Setup

  1. In Google Play Console, create subscription products
  2. In RevenueCat, create a Google Play app and add the products
  3. Upload your Google Play service account JSON to RevenueCat
  4. Configure Real-time Developer Notifications (RTDN) topic

Sandbox Testing Guide

Never test purchases with real money during development. Both platforms provide sandbox environments:

  1. iOS Sandbox — Create a Sandbox Apple ID in App Store Connect. Subscriptions auto-renew every 5 minutes (monthly) and expire after 6 renewals.
  2. Android Test — Add license testers in Google Play Console. Test purchases are free and auto-renew on an accelerated schedule.
  3. RevenueCat Debug Mode — Use Purchases.setLogLevel(LogLevel.debug) to see every API call in the Flutter console.
  4. 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.purchaseCancelledError and 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

  1. Remove debug logging in production builds
  2. Verify API keys are correct per platform
  3. Test all purchase flows in sandbox (iOS) and test mode (Android)
  4. Ensure Restore Purchases button exists and works
  5. Handle the "no offerings" state gracefully
  6. Verify entitlement names match the RevenueCat dashboard
  7. Set up App Store Server Notifications and Google RTDN
  8. Test on real devices for both platforms
  9. Verify paywall shows correct localized prices
  10. 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.

Share this article

Ready to ship your Flutter app faster?

The Flutter Kit gives you a production-ready Flutter codebase with onboarding, paywalls, auth, AI integrations, and more. Stop building boilerplate. Start building your product.

Get The Flutter Kit