The Flutter Kit logoThe Flutter Kit
Guide

How to Add Payments to a Flutter App with RevenueCat

A hands-on walkthrough of hand-rolling a RevenueCat paywall in Flutter — configuring offerings, gating features on entitlements, and wiring restore — or skipping the entire plumbing layer with The Flutter Kit.

Last updated: 2026-06-10 9 min read By Ahmed Gagan, Flutter Engineer
Quick Answer

To add payments to a Flutter app, the cleanest path is RevenueCat: it wraps StoreKit 2 on iOS and Play Billing on Android behind a single Dart SDK so you fetch offerings, present a paywall, purchase a package, and read entitlements without touching native receipt validation. You can hand-roll this yourself in a day or two, or skip the plumbing entirely with The Flutter Kit ($69 one-time, lifetime updates, full source ownership), which ships a configured paywall, entitlement gating, and restore flow out of the box.

What handles the payment
RevenueCat over StoreKit 2 (iOS) + Play Billing (Android)
Build-it-yourself time
~1-2 days for offerings, paywall, entitlements, restore
Skip-it option
The Flutter Kit ships it pre-wired — $69 one-time

Why hand-rolling the paywall is more work than the happy path looks

The getOfferings-to-purchasePackage flow fits in a tweet, and that is exactly the trap. The real cost of adding payments to a Flutter app lives in the edges: a Restore button that App Review will reject your build without, deferred/pending purchases on Android that complete minutes later, sandbox accounts that behave differently from production, entitlement listeners that have to survive hot restart, and the discipline to never cache a 'isPremium' boolean that outlives a refund. Each of these is a small thing you discover one rejection or one angry refunded user at a time. Hand-rolling teaches you the model — which is genuinely worth doing once — but you are then signing up to maintain receipt-edge handling across two app stores and every SDK bump.

  • Apple rejects paywalls with no visible Restore Purchases action
  • Android pending purchases resolve asynchronously and must be reconciled
  • Entitlements must be the source of truth, never a local boolean
  • Sandbox and production purchase behaviour diverge and need separate testing

When you should NOT use a boilerplate for this

Honesty first: if you already have a working RevenueCat integration, or your app has a single non-consumable unlock with no subscriptions, hand-rolling is the right call — pulling in a whole boilerplate to render one paywall is overkill. The same is true if your monetization is genuinely unusual (metered billing, B2B invoicing, Stripe-on-web only) where RevenueCat is not the right tool and you should look at a Stripe-based flow instead. The Flutter Kit earns its $69 when you are starting fresh and want the offerings fetch, the Material 3 paywall, the entitlement gating, the restore flow, and the user-identity sync already assembled and tested together — so payments become a config step, not a multi-day plumbing project. If you only need the payment piece in isolation, our standalone RevenueCat walkthrough covers it without the kit.

Hand-roll a RevenueCat paywall in Flutter (or skip it)

These steps walk through building the payment layer yourself with the purchases_flutter SDK. Each one is a real piece of plumbing The Flutter Kit already wires up for you — do it by hand to understand it, then decide whether you want to maintain it.

  1. 1

    Add the RevenueCat SDK and a billing entitlement

    Add purchases_flutter (and optionally purchases_ui_flutter for the prebuilt paywall) to pubspec.yaml. On iOS enable the In-App Purchase capability in Xcode; on Android the Play Billing dependency is pulled in transitively. Create your products and an offering in the RevenueCat dashboard before you write any Dart.

    flutter pub add purchases_flutter purchases_ui_flutter
    cd ios && pod install
  2. 2

    Configure Purchases once at app startup

    Initialize the SDK with your platform-specific public API key in main(), before runApp. Keep the iOS and Android keys separate and never ship a secret key. This is the single line that connects your app to the StoreKit / Play Billing bridge.

    await Purchases.setLogLevel(LogLevel.debug);
    final config = PurchasesConfiguration(
      Platform.isIOS ? appleKey : googleKey,
    );
    await Purchases.configure(config);
  3. 3

    Fetch the current offering

    Call getOfferings() and read the current offering's available packages. Each package maps to a store product with localized price strings already formatted by the store — never hard-code prices. Wrap this in a Cubit or repository so the UI just listens for loaded packages.

    final offerings = await Purchases.getOfferings();
    final packages = offerings.current?.availablePackages ?? [];
    // packages.first.storeProduct.priceString -> e.g. "$9.99"
  4. 4

    Build the paywall UI in Material 3

    Render the packages as selectable cards using your design tokens, with a primary CTA, a restore link, and links to your terms and privacy policy (the stores reject paywalls missing these). If you'd rather not design pixels, RevenueCat's purchases_ui_flutter can present a dashboard-configured paywall instead.

  5. 5

    Purchase a package and handle every error path

    Call purchasePackage and handle the cases that actually happen in production: user cancellation, pending purchases, network failures, and already-owned products. Treat PurchasesErrorCode.purchaseCancelledError as a no-op, not an error toast.

    try {
      final result = await Purchases.purchasePackage(pkg);
      final isPro = result.customerInfo
          .entitlements.active.containsKey('pro');
    } on PlatformException catch (e) {
      final code = PurchasesErrorHelper.getErrorCode(e);
      if (code != PurchasesErrorCode.purchaseCancelledError) rethrow;
    }
  6. 6

    Gate features on entitlements, not on a local flag

    The source of truth for 'is this user paid?' is customerInfo.entitlements.active. Listen with addCustomerInfoUpdateListener so renewals, cancellations, and cross-device restores update your UI reactively. Persisting a boolean in shared prefs is the classic bug that lets refunded users keep premium.

    Purchases.addCustomerInfoUpdateListener((info) {
      final isPro = info.entitlements.active.containsKey('pro');
      context.read<SubscriptionCubit>().update(isPro);
    });
  7. 7

    Wire restore and identify the user

    Add a Restore Purchases button (Apple requires it) that calls Purchases.restorePurchases(). When the user signs in, call Purchases.logIn(uid) so entitlements follow the account across devices instead of being tied to an anonymous app-user ID.

    await Purchases.logIn(firebaseUser.uid);
    final restored = await Purchases.restorePurchases();

Frequently Asked Questions

Do I need RevenueCat to add payments to a Flutter app, or can I use in_app_purchase directly?
You can use the official in_app_purchase plugin, but then you own receipt validation, cross-platform entitlement reconciliation, and renewal tracking yourself. RevenueCat wraps StoreKit 2 and Play Billing behind one Dart API and gives you entitlements for free, which is why most Flutter teams reach for it. For a single one-time unlock with no subscriptions, in_app_purchase alone can be enough.
How long does it realistically take to hand-roll the RevenueCat paywall in Flutter?
The happy-path code is an afternoon. Getting it production-ready — Restore button, error-code handling, Android pending purchases, sandbox testing on both stores, and a reactive entitlement listener that survives hot restart — is closer to one to two focused days, plus the store-dashboard configuration. That gap is exactly the plumbing The Flutter Kit pre-builds.
Why should entitlements be the source of truth instead of saving a premium flag locally?
A local boolean does not know about refunds, cancellations, billing retries, or a purchase made on another device. customerInfo.entitlements.active reflects the live subscription state, and addCustomerInfoUpdateListener pushes changes to your UI. Caching isPremium in shared preferences is the most common payments bug — it lets refunded users keep access until the app reinstalls.
Will RevenueCat handle both iOS subscriptions and Android billing from the same Dart code?
Yes. The purchases_flutter SDK presents one offering and one package model regardless of platform; under the hood it talks to StoreKit 2 on iOS and Play Billing on Android. You configure separate public API keys per platform, but your paywall UI and entitlement-gating logic are written once in Dart.
Does The Flutter Kit lock me into RevenueCat for payments?
No. Because you get full source ownership with the $69 one-time license, the RevenueCat module is feature-flagged and replaceable. If you later need a Stripe-based web flow or a different billing provider, you can swap the implementation behind the same subscription Cubit interface rather than rewriting the app around it.
Can I test the paywall without spending real money?
Yes. On iOS use a StoreKit configuration file or a sandbox Apple ID; on Android use a license-tester account and an internal testing track. RevenueCat surfaces sandbox transactions in its dashboard so you can confirm entitlements activate before going live. Whether you hand-roll or use the kit, do this on both stores before launch.

Keep exploring

Skip the payments plumbing

The Flutter Kit ships a configured RevenueCat paywall, entitlement gating, restore, and user-identity sync already wired together — $69 one-time, lifetime updates, full source ownership. Build payments as a config step, not a two-day project.

Get The Flutter Kit — $69

One-time purchase · Lifetime updates · Unlimited projects