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.
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.
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
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
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
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
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
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
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
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?
How long does it realistically take to hand-roll the RevenueCat paywall in Flutter?
Why should entitlements be the source of truth instead of saving a premium flag locally?
Will RevenueCat handle both iOS subscriptions and Android billing from the same Dart code?
Does The Flutter Kit lock me into RevenueCat for payments?
Can I test the paywall without spending real money?
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 — $69One-time purchase · Lifetime updates · Unlimited projects