# 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_purchaseplugin - 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#
| Requirement | Version | Notes |
|---|---|---|
| Flutter | 3.19+ | Works on newer versions too |
| Dart | 3+ | Matches Flutter stable |
| iOS | Xcode 15+ | Required for modern iOS builds |
| Android | AGP 8+ | Use current Android Gradle Plugin |
| App Store Connect account | Active | With agreements and banking completed |
| Google Play Console account | Active | With payments profile set up |
| Real device testing | Recommended | Emulators 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
| Capability | in_app_purchase only | RevenueCat |
|---|---|---|
| Basic purchase flow | Yes | Yes |
| Receipt validation | You build it | Included |
| Sub renewals and cancellations | You track it | Included |
| Entitlements abstraction | You build it | Included |
| Paywall offerings | You build it | Included |
| Restore purchases | You implement | Simplified helpers |
| Cross-platform subscription state | Harder | Easier |
| Debugging tooling | Minimal | Dashboard 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_monthlyorpro_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#
| Approach | How it works | Best for |
|---|---|---|
| Server-side validation | Your backend validates Apple and Google receipts and stores current entitlement state | Teams with backend + billing expertise |
| RevenueCat | SDK sends purchase info, RC validates and tracks entitlement state, app checks entitlements | Most 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#
# pubspec.yaml
dependencies:
in_app_purchase: ^3.2.0
in_app_purchase_storekit: ^0.3.20
in_app_purchase_android: ^0.4.0Keep versions aligned with your Flutter stable channel.
2) Query products#
You need the exact store product IDs.
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.
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
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.
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
prois 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#
# pubspec.yaml
dependencies:
purchases_flutter: ^8.3.0Initialize early, usually after app start and after you know a stable app user id if you have accounts.
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.
Future<void> loginToRevenueCat(String appUserId) async {
await Purchases.logIn(appUserId);
}4) Fetch offerings and render a paywall#
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:
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.
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#
| Item | Why it matters | Example |
|---|---|---|
| Clear price and period | Prevents misleading UX | “€4.99 per month” |
| Trial disclosure | Mandatory if you offer trials | “7-day free trial, then €4.99 per month” |
| Restore purchases button | Required on iOS | “Restore purchases” |
| Manage subscription link | Reduces support tickets | Link to system subscription management |
| Terms and privacy links | Review 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#
- 1Create sandbox tester in App Store Connect
- 2Install the app from TestFlight
- 3Trigger purchase flow
- 4Login 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#
- 1Upload AAB to Internal testing
- 2Add yourself as tester and license tester
- 3Install from Play Store testing link
- 4Trigger 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#
| Platform | Signal you are in sandbox | Quick check |
|---|---|---|
| iOS | Sandbox login prompt and sandbox subscription management | Settings app shows sandbox account context |
| Android | Test purchase dialog and test card behavior | Play 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:
- 1Log queried IDs and environment build flavor
- 2Confirm install source and track
- 3Confirm product state and metadata completeness
- 4Test 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:
- 1After purchase, call
getCustomerInfoand check active entitlements - 2In RevenueCat dashboard, inspect the customer timeline
- 3On iOS, confirm the correct Apple ID was used
- 4Retry 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:
- 1Verify signing key and package name
- 2Confirm the device Play account is the tester account
- 3Clear Play Store cache only as a last resort
- 4Use 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.
| Area | Check | Why |
|---|---|---|
| Products | IDs, pricing, localization | Prevent empty product lists and rejections |
| Paywall | Restore, terms, clear pricing | Review compliance and trust |
| Access control | Entitlements, not local flags | Prevent fraud and stale access |
| Testing | TestFlight and Internal testing | Real store behavior |
| Observability | Logs for purchase steps | Faster debugging |
| Release | Automated builds and versioning | Reduce 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
Founder & Senior Developer at Samioda. 8+ years building React, Next.js, Flutter and n8n automation solutions for clients across Europe.
More in Mobile Development
All →Flutter Local Database Comparison: Hive vs Isar vs sqflite vs Drift (2026 Guide)
A practical Flutter local database comparison for 2026: Hive vs Isar vs sqflite vs Drift, with guidance on performance, queries, migrations, encryption, and web support for common app types.
Flutter Animations in Production: Implicit vs Explicit, Rive and Lottie, and Performance Tips
A practical Flutter animations guide for production apps: when to use implicit vs explicit animations, proven AnimationController patterns, Hero transitions, Rive and Lottie tradeoffs, plus performance and testing strategies to keep 60fps.
Flutter + Supabase vs Firebase in 2026: Auth, Realtime, Offline, Pricing, and Lock-In
A practical 2026 comparison of Flutter with Supabase vs Firebase across auth, push, realtime, offline/local-first, storage, functions, pricing, and vendor lock-in — with recommendations by app type and scale.
Need help with your project?
We build custom solutions using the technologies discussed in this article. Senior team, fixed prices.
Related Articles
Flutter vs Native iOS/Android in 2026: Cost, Performance, and Time-to-Market Tradeoffs
A practical, numbers-driven comparison of Flutter vs native iOS and Android for 2026 — including cost model, performance reality, maintenance impact, and a decision framework for MVPs, high-performance UI, heavy platform APIs, and regulated apps.
Flutter App Development Cost in 2026: Complete Pricing Guide
How much does a Flutter app cost in 2026? Complete pricing breakdown by app complexity, features, and development approach. Real cost examples included.
How Much Does a Mobile App MVP Cost? Realistic Breakdown (2026)
Mobile app MVP cost explained with real feature-based breakdowns, app-type ranges, and a Flutter vs native comparison to budget your MVP realistically.