Mobile Development
FlutterMobile DevelopmentIn-App PurchasesSubscriptionsRevenueCatiOSAndroid

Flutter In-App Purchases & Subscriptions: Apple and Google Setup, Testing, and RevenueCat

AO
Adrijan Omićević
·15 min read

# What You’ll Build and Why It Matters#

This guide walks through implementing Flutter in app purchases for both one-time purchases and subscriptions, with two paths:

  • Direct store integration using the in_app_purchase plugin
  • A production-friendly approach using RevenueCat for receipt validation, entitlements, paywalls, and subscriber state sync

If you ship subscriptions, getting this wrong is expensive. Apple and Google can revoke access based on refunds, chargebacks, grace periods, billing retries, and upgrades or downgrades, and a client-only approach will eventually mis-grant access.

We’ll cover store setup, implementation, receipt validation concepts, sandbox testing, and a dedicated section for debugging common production failures.

# Prerequisites#

RequirementVersionNotes
Flutter3.19+Works on newer versions too
Dart3+Matches Flutter stable
iOSXcode 15+Required for modern iOS builds
AndroidAGP 8+Use current Android Gradle Plugin
App Store Connect accountActiveWith agreements and banking completed
Google Play Console accountActiveWith payments profile set up
Real device testingRecommendedEmulators can be limited for billing flows

If your release process is still manual, fix that early. IAP issues are often environment-specific and you will rebuild frequently. See our CI guide: Flutter CI/CD with GitHub Actions, Codemagic, and Fastlane.

# Decide Your Architecture: Direct Stores vs RevenueCat#

You can build IAP in Flutter in two common ways.

Option A: Store-native with in_app_purchase#

Pros:

  • No extra vendor dependency
  • Lower recurring cost
  • Full control

Cons:

  • You own receipt validation, renewals, refunds, and edge cases
  • Cross-platform subscriber state is harder
  • Restoring purchases and entitlement logic must be rock solid

Option B: RevenueCat on top of stores#

Pros:

  • Server-side receipt validation and state management
  • Entitlements and offerings unify iOS and Android
  • Built-in paywalls and A B testing support
  • Better observability for subscriber status changes

Cons:

  • Vendor cost at scale
  • You still need correct store configuration
  • Another SDK and dashboard to maintain
Capabilityin_app_purchase onlyRevenueCat
Basic purchase flowYesYes
Receipt validationYou build itIncluded
Sub renewals and cancellationsYou track itIncluded
Entitlements abstractionYou build itIncluded
Paywall offeringsYou build itIncluded
Restore purchasesYou implementSimplified helpers
Cross-platform subscription stateHarderEasier
Debugging toolingMinimalDashboard events and logs

🎯 Key Takeaway: If subscriptions are a core revenue line, use RevenueCat unless you already have a mature backend and billing expertise.

# Product Setup on Apple: App Store Connect#

Apple’s setup determines whether your products can even be fetched in sandbox. Most “product not found” bugs start here.

1) Create the app and enable IAP capability#

  • Create your app in App Store Connect with the correct bundle ID
  • In Xcode, enable the In-App Purchase capability for the target
  • Ensure the bundle ID matches exactly across Xcode, App Store Connect, and any environments

2) Create In-App Purchase products#

Apple has two common IAP types for most Flutter apps:

  • Consumable: coins, credits, one-time items that can be repurchased
  • Non-consumable: lifetime unlock
  • Auto-renewable subscription: monthly or yearly plans

For subscriptions you will also configure:

  • Subscription Group: users can only have one active subscription per group
  • Subscription Levels: for upgrades and downgrades

Minimum checklist per product:

  • Reference name
  • Product ID, for example com.samioda.app.pro.monthly
  • Pricing
  • Localization display name and description
  • Review screenshot for some cases

3) Agreements, tax, and banking#

If agreements are not accepted or banking is incomplete, purchases can fail or products might not appear as expected in testing.

4) Sandbox testers and testing distribution#

Apple sandbox testing expectations that trip teams up:

  • For iOS, install via TestFlight for realistic testing. Local debug installs can fetch products, but you will debug fewer “real-world” cases.
  • Use a sandbox Apple ID created in App Store Connect users and access.
  • On the device, sign out of the normal Apple ID inside the App Store purchase flow when prompted, then log in with the sandbox tester.

⚠️ Warning: Apple sandbox subscription renewals are accelerated and may renew multiple times quickly. Your app must handle multiple renewal events without re-granting consumables or duplicating access records.

# Product Setup on Google: Play Console#

Google Play Billing is strict about testing tracks and accounts.

1) Create the app and configure billing#

  • Create the app in Play Console
  • Complete payments profile and any required verification
  • Ensure your app is signed correctly and uploaded to a testing track at least once

2) Create products and subscriptions#

In Play Console:

  • Monetize section for in-app products and subscriptions
  • Create product IDs like pro_monthly or pro_yearly
  • Add base plans and offers for subscriptions if you use them

In 2026, Google subscriptions commonly use:

  • Base plans: define billing period and renewal
  • Offers: trial, intro pricing, regional discounts

3) License testers and internal testing#

Testing expectations:

  • Add your Gmail account as a license tester
  • Upload an App Bundle to Internal testing
  • Install from the Play Store internal testing link

If you install via Android Studio directly, Billing may behave differently and often fails to retrieve live products.

# Receipt Validation and Why Client-Only Is Not Enough#

You can start with client-side purchase flows, but access control must rely on a trusted source.

What can go wrong without validation#

  • A jailbroken device can fake purchase states
  • A subscription can be refunded or charged back
  • Billing can enter grace period or account hold
  • A user can cancel renewal but still be active until period end
  • Google can pause or defer billing and renew later

Your access decision should be based on a verified subscription state, not “the user told us they paid”.

Two practical approaches#

ApproachHow it worksBest for
Server-side validationYour backend validates Apple and Google receipts and stores current entitlement stateTeams with backend + billing expertise
RevenueCatSDK sends purchase info, RC validates and tracks entitlement state, app checks entitlementsMost subscription apps

If your app also relies on user accounts, combine subscriber state with your auth model. If you’re implementing auth and notifications, keep environments clean. See Flutter push notifications with FCM and APNs in production.

# Implement Flutter In-App Purchases with in_app_purchase#

This is the lowest-level Flutter approach. You can later migrate to RevenueCat.

1) Add dependencies#

YAML
# pubspec.yaml
dependencies:
  in_app_purchase: ^3.2.0
  in_app_purchase_storekit: ^0.3.20
  in_app_purchase_android: ^0.4.0

Keep versions aligned with your Flutter stable channel.

2) Query products#

You need the exact store product IDs.

Dart
import 'package:in_app_purchase/in_app_purchase.dart';
 
final iap = InAppPurchase.instance;
 
Future<List<ProductDetails>> fetchProducts() async {
  final ids = <String>{
    'com.samioda.app.pro.monthly',
    'com.samioda.app.pro.yearly',
  };
 
  final available = await iap.isAvailable();
  if (!available) return [];
 
  final response = await iap.queryProductDetails(ids);
  if (response.error != null) {
    throw Exception('IAP query error: ${response.error}');
  }
  return response.productDetails;
}

Common causes of empty results:

  • Wrong product IDs
  • App installed outside TestFlight or Play internal testing
  • Product not in “Ready to submit” state, missing metadata, or not cleared for testing

3) Start a purchase#

For non-consumables and subscriptions, use buyNonConsumable. For consumables, use buyConsumable and decide how you grant and record credits.

Dart
Future<void> buy(ProductDetails product) async {
  final param = PurchaseParam(productDetails: product);
  await iap.buyNonConsumable(purchaseParam: param);
}

4) Listen to purchase updates and finish transactions#

Purchase updates are streamed. You must:

  • verify purchase
  • grant access
  • complete the transaction
Dart
late final StreamSubscription<List<PurchaseDetails>> sub;
 
void startListening() {
  sub = iap.purchaseStream.listen((purchases) async {
    for (final p in purchases) {
      if (p.status == PurchaseStatus.purchased ||
          p.status == PurchaseStatus.restored) {
        final ok = await verifyOnServer(p);
        if (ok) {
          await grantEntitlement(p.productID);
        }
      }
      if (p.pendingCompletePurchase) {
        await iap.completePurchase(p);
      }
    }
  });
}

verifyOnServer is the hard part. For subscriptions, verification is not optional if you want accurate access control.

5) Restore purchases#

Users expect restore to work on iOS. It also matters after reinstall.

Dart
Future<void> restore() async {
  await iap.restorePurchases();
}

Restoring is not a full solution for subscriptions unless you also verify current status and handle expiration, refunds, and renewals.

💡 Tip: Treat “restore” as a trigger to refresh verified subscriber state, not as proof of active access.

# Implementing Subscriptions with RevenueCat#

RevenueCat removes most receipt complexity and gives you a clean entitlement check.

1) Create RevenueCat project and connect stores#

In RevenueCat:

  • Create a project
  • Add iOS and Android apps
  • Connect App Store Connect and Google Play
  • Import products

Key concept: Offerings and Entitlements.

2) Define entitlements and map products#

Example:

  • Entitlement: pro
  • Products: monthly and yearly subscriptions
  • If the subscription is active, entitlement pro is active

This prevents your app logic from being tied to specific product IDs. You can change pricing and products later without app updates.

3) Add dependency and initialize#

YAML
# pubspec.yaml
dependencies:
  purchases_flutter: ^8.3.0

Initialize early, usually after app start and after you know a stable app user id if you have accounts.

Dart
import 'package:purchases_flutter/purchases_flutter.dart';
 
Future<void> initRevenueCat() async {
  await Purchases.setLogLevel(LogLevel.info);
 
  await Purchases.configure(
    PurchasesConfiguration('public_sdk_key_here'),
  );
}

If you have authentication, identify users so purchases follow them across devices.

Dart
Future<void> loginToRevenueCat(String appUserId) async {
  await Purchases.logIn(appUserId);
}

4) Fetch offerings and render a paywall#

Dart
Future<Offering?> fetchPaywall() async {
  final offerings = await Purchases.getOfferings();
  return offerings.current;
}

Your paywall UI should show:

  • plan name
  • price and period
  • trial if available
  • restore button
  • clear terms and manage subscription link

When the user selects a package:

Dart
Future<void> purchasePackage(Package pkg) async {
  final result = await Purchases.purchasePackage(pkg);
  final proActive = result.customerInfo.entitlements.active.containsKey('pro');
  if (!proActive) throw Exception('Purchase completed but pro not active');
}

5) Check entitlement anywhere#

Your app should gate features based on entitlements, not a local boolean.

Dart
Future<bool> hasPro() async {
  final info = await Purchases.getCustomerInfo();
  return info.entitlements.active.containsKey('pro');
}

This handles renewals, cancellations, refunds, and cross-device restores more reliably than manual purchase history checks.

# Paywalls That Convert Without Getting Rejected#

A paywall is both product and compliance. Apple and Google reject confusing pricing or missing restore and terms.

Practical paywall checklist#

ItemWhy it mattersExample
Clear price and periodPrevents misleading UX“€4.99 per month”
Trial disclosureMandatory if you offer trials“7-day free trial, then €4.99 per month”
Restore purchases buttonRequired on iOS“Restore purchases”
Manage subscription linkReduces support ticketsLink to system subscription management
Terms and privacy linksReview compliance“Terms” and “Privacy”

Keep paywall logic simple:

  • 1 primary CTA for the recommended plan
  • monthly and yearly choices
  • emphasize yearly savings with real math, for example “€49.99 per year equals €4.17 per month”

If you need to estimate budget and timeline for adding subscriptions and paywalls, use this pricing context: Flutter app cost in 2026.

# Sandbox and Test Environment Setup#

Most billing bugs are “not actually running in sandbox”.

Apple sandbox testing workflow#

  1. 1
    Create sandbox tester in App Store Connect
  2. 2
    Install the app from TestFlight
  3. 3
    Trigger purchase flow
  4. 4
    Login with the sandbox Apple ID when prompted

Expected behavior:

  • Subscription renewals happen quickly in sandbox
  • You may see multiple renewals and expirations in minutes
  • Cancellation and renewal toggles are done in iOS settings for the sandbox account

Google testing workflow#

  1. 1
    Upload AAB to Internal testing
  2. 2
    Add yourself as tester and license tester
  3. 3
    Install from Play Store testing link
  4. 4
    Trigger purchases in the app

Expected behavior:

  • Purchase dialogs show “test” indications for license testers
  • Subscription lifecycle events are faster than production

Validate your test signals#

PlatformSignal you are in sandboxQuick check
iOSSandbox login prompt and sandbox subscription managementSettings app shows sandbox account context
AndroidTest purchase dialog and test card behaviorPlay Store account is a tester and app installed from Play

# Common Production Issues and How to Debug Them#

This section is the difference between “it worked once” and stable revenue.

Issue 1: Products return empty list#

Most likely causes:

  • Product IDs mismatch
  • App installed via sideload or debug, not TestFlight or Play testing
  • Products not approved, missing localization, or not “Ready”
  • Store account country mismatch with product availability

Debug steps:

  1. 1
    Log queried IDs and environment build flavor
  2. 2
    Confirm install source and track
  3. 3
    Confirm product state and metadata completeness
  4. 4
    Test with a fresh sandbox tester or license tester account

Issue 2: Purchase completes but entitlement is not active#

Typical causes:

  • You completed the purchase before verification finished
  • Your entitlement mapping is wrong in RevenueCat
  • You are checking a local cache instead of refreshed customer info
  • Network issues during post-purchase sync

Debug steps:

  1. 1
    After purchase, call getCustomerInfo and check active entitlements
  2. 2
    In RevenueCat dashboard, inspect the customer timeline
  3. 3
    On iOS, confirm the correct Apple ID was used
  4. 4
    Retry fetch on app resume and after a short delay

Issue 3: Duplicate grants or missing consumables#

Consumables require idempotency. If your app grants credits twice, you will leak revenue.

Fix pattern:

  • Store a transaction ID and only grant once
  • Make granting atomic on the backend if possible

Issue 4: iOS “Ask to Buy” and family sharing surprises#

If you support family sharing or encounter parental approval flows, purchases may go into pending. Your UI must handle PurchaseStatus.pending and provide a clear state.

Issue 5: Google subscriptions show active in Play but not in app#

Often caused by:

  • Wrong Google account on the device
  • Play services cache issues
  • App not signed with the same key as the uploaded track build

Debug steps:

  1. 1
    Verify signing key and package name
  2. 2
    Confirm the device Play account is the tester account
  3. 3
    Clear Play Store cache only as a last resort
  4. 4
    Use RevenueCat dashboard or your backend logs to confirm validation events

Issue 6: Webhooks and backend not updating after renewals#

If you use RevenueCat webhooks or store server notifications, a missed webhook can cause stale access on your side.

Mitigations:

  • Always allow the app to refresh entitlement state on launch and resume
  • Make webhook processing idempotent and retry-safe
  • Keep a “last verified at” timestamp and re-check periodically

ℹ️ Note: Subscriber state is event-driven, but your app must be resilient to missed events. A periodic entitlement refresh is cheap and prevents long-lived incorrect access.

# Operational Checklist Before You Launch#

Use this as a last-mile list for production readiness.

AreaCheckWhy
ProductsIDs, pricing, localizationPrevent empty product lists and rejections
PaywallRestore, terms, clear pricingReview compliance and trust
Access controlEntitlements, not local flagsPrevent fraud and stale access
TestingTestFlight and Internal testingReal store behavior
ObservabilityLogs for purchase stepsFaster debugging
ReleaseAutomated builds and versioningReduce human error

If your build and release pipeline is fragile, your billing fixes will take longer and cause churn. Set up automation early: Flutter CI/CD with GitHub Actions, Codemagic, and Fastlane.

# Key Takeaways#

  • Use entitlements as the access layer, not raw product IDs, to keep billing logic stable across pricing and product changes.
  • For subscriptions, avoid client-only logic and use server-side validation or RevenueCat to handle renewals, refunds, grace periods, and chargebacks.
  • Always test IAP from real distribution channels: TestFlight on iOS and Play Internal testing on Android, otherwise you will chase fake bugs.
  • Build purchase handling to be idempotent, especially for consumables, to prevent duplicate grants and revenue leakage.
  • When debugging “purchase succeeded but access missing”, inspect the customer timeline, refresh customer info after purchase, and verify store account and signing keys.

# Conclusion#

Flutter in app purchases are straightforward to demo and surprisingly easy to break in production if you skip validation, entitlements, and real sandbox testing. Set up products correctly in App Store Connect and Play Console, implement purchase flows with clear paywalls, and rely on verified entitlement state instead of local flags.

If you want Samioda to implement subscriptions end-to-end, including RevenueCat setup, paywall UX, webhook integration, and release automation, contact us and we’ll scope it into a production-ready delivery plan that matches your timeline and budget.

FAQ

Share
A
Adrijan OmićevićFounder & Senior Developer

Founder & Senior Developer at Samioda. 8+ years building React, Next.js, Flutter and n8n automation solutions for clients across Europe.

Need help with your project?

We build custom solutions using the technologies discussed in this article. Senior team, fixed prices.