Level Up Your Flutter App: The No-Sweat Guide to Adding Subscriptions with RevenueCat (iOS & Android) πΈπ

Hey Flutter Devs! So, you've built an awesome app (maybe even with a little help from our AI overlords π). That's fantastic! But now comes the tricky part: actually making some moolah from it. Setting up subscriptions can feel like wrestling a digital octopus β so many tentacles, so much confusion! π
Fear not! This guide is your friendly co-pilot. We're going to walk through adding Premium and VIP subscription plans (both monthly and annual) to your Flutter app using RevenueCat. We'll do it step-by-step, no crazy jargon, just a clear path from zero to "Cha-ching!" π°
What We'll Cover (The Grand Plan!):
Store Setup (The Boring but Necessary Stuff): Getting your products ready on the App Store (iOS) and Google Play Store (Android).
RevenueCat Magic β¨: Connecting the stores to RevenueCat, defining what users get (Entitlements), and setting up your "for sale" signs (Offerings & Packages).
Flutter Power-Up π¨βπ»π©βπ»: Integrating the RevenueCat SDK into your app to show plans, handle purchases, and check who gets the cool features.
Testing Time π€: Making sure it all actually works!
Let's dive in!
Part 0: Before You Begin - The Essentials π οΈ
Make sure you have these ready:
Apple Developer Account: If you're targeting iOS, you need this. Sign up at developer.apple.com/programs. Yes, there's an annual fee.
Google Play Console Account: For Android, this is your mothership. Sign up at play.google.com/console/developers. There's a one-time fee.
RevenueCat Account: This is our subscription superhero. Grab a free account at revenuecat.com.
Your Flutter App Project: Obviously! π
Got all that? Sweet!
Part 1: Store Setup - Laying the Groundwork ποΈ
We need to tell Apple and Google what you're selling. We'll set up four types of subscription products for this example:
Premium Monthly (e.g., $0.99/month)
Premium Annual (e.g., $9.99/year - a little discount for committing!)
VIP Monthly (e.g., $9.99/month)
VIP Annual (e.g., $99.99/year)
Section A: iOS - App Store Connect Shenanigans π
1. Register Your App's Bundle ID
Go to
Developer Account > Certificates, Identifiers & Profiles > Identifiers
.Click the
+
sign. Select "App IDs," then "App."Description: Your app name (e.g.,
AiVoiceJournal
).Bundle ID: Choose "Explicit" and enter a unique ID (e.g.,
com.yourcompany.aivoicejournal
). SAVE THIS BUNDLE ID!Capabilities: Scroll down and enable In-App Purchase.
Click "Continue," then "Register." Done!
2. Create Your App in App Store Connect
Head to appstoreconnect.apple.com and go to "My Apps."
Click
+
> "New App."Platforms: Select iOS.
Name: Your app's name.
Primary Language: Choose one.
Bundle ID: Select the one you just registered.
SKU: A unique ID for your reference (e.g.,
aivoicejournal_001
).User Access: "Full Access."
Click "Create."
Pro-Tip: Fill out some basic app info like category and privacy policy URL now to avoid headaches later.
3. Generate an App-Specific Shared Secret (Say that five times fast!)
Inside your app in App Store Connect:
General > App Information
.Scroll to "App-Specific Shared Secret."
Click "Manage" > "Generate App-Specific Shared Secret."
Copy this secret and keep it safe! We'll need it for RevenueCat.
4. Create an App Store Connect API Key (The .p8 File)
Still in App Store Connect (but at the account level, not inside a specific app):
Users and Access > Keys
(orIntegrations > App Store Connect API
).Click the
+
sign next to "Keys."Name: Something like
RevenueCat Key
.Access: Choose "App Manager."
Click "Generate."
SUPER IMPORTANT:
Download the .p8 file IMMEDIATELY. You only get one chance! Save it securely.
Copy the Issuer ID.
Copy the Key ID.
Keep these three pieces of info safe with your .p8 file.
5. Set Up Your Subscription Products
Back in your app on App Store Connect:
Monetization > Subscriptions
.Create a Subscription Group:
Click
+
next to "Subscription Groups."Name: e.g.,
My Awesome Plans
. Click "Create."
Add Your Subscription Products (Repeat for all 4 plans):
Inside your new group, click the
+
to add a subscription.Let's do "VIP Monthly" ($9.99) first as an example:
Reference Name: For your eyes only (e.g.,
VIP Monthly iOS
).Product ID: A unique string (e.g.,
com.yourcompany.aivoicejournal.vip
.monthly
). SAVE THIS!Subscription Display Name: What users see (e.g.,
VIP Plan
).Description: What users get (e.g.,
Unlock ALL amazing VIP perks!
).Subscription Duration: "1 Month."
Subscription Price: Click
+
, select a region (e.g., US), and set the price (e.g., $9.99). Confirm. Apple will auto-convert for other regions (you can edit later).Localization: Add at least one (e.g., English US) with a display name and description.
Review Information: Add a screenshot (even a placeholder for now) and brief notes for the reviewer.
Click "Save" (top right).
Now, repeat the "Add Subscription" process for:
Premium Monthly iOS (e.g.,
com.yourcompany.aivoicejournal.premium.monthly
, $0.99, 1 Month)Premium Annual iOS (e.g.,
com.yourcompany.aivoicejournal.premium.annual
, $9.99, 1 Year)VIP Annual iOS (e.g.,
com.yourcompany.aivoicejournal.vip
.annual
, $99.99, 1 Year)
Make sure each is saved and its status is something like "Ready to Submit."
Phew! iOS store setup done. Grab a coffee. β
Section B: Android - Google Play Console Fun & Games π€
1. Create Your App in Google Play Console
Log in to play.google.com/console.
Click "Create app."
Fill in: App name, default language, App (not Game), Free.
Agree to the terms. Click "Create app."
2. Prepare Your Flutter App for Release (Signing)
This is crucial for uploading to Google Play.
Generate a Keystore (
.jks
file):If you don't have one, open your terminal and run:
keytool -genkey -v -keystore /path/to/your/key/upload-keystore.jks -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 -alias upload
(Replace
/path/to/your/key/
with where you want to save it, e.g., insideandroid/app/
).- Remember the passwords you set! Store them securely.
Create
android/
key.properties
file:storePassword=YOUR_KEYSTORE_PASSWORD keyPassword=YOUR_KEY_PASSWORD keyAlias=upload storeFile=app/upload-keystore.jks
(Adjust
storeFile
if your.jks
is not inandroid/app/
. Add this file to.gitignore
!)Configure
android/app/build.gradle.kts
(orbuild.gradle
if you have the older Groovy version): You need to tell Gradle to use these signing details for release builds. For.kts
(Kotlin DSL):// At the top of android/app/build.gradle.kts import java.util.Properties import java.io.FileInputStream // Before the `android { ... }` block val keystoreProperties = Properties() val keystorePropertiesFile = rootProject.file("key.properties") // Assumes key.properties is in android/ if (keystorePropertiesFile.exists()) { keystoreProperties.load(FileInputStream(keystorePropertiesFile)) } android { // ... signingConfigs { create("release") { if (keystorePropertiesFile.exists()) { keyAlias = keystoreProperties["keyAlias"] as String? keyPassword = keystoreProperties["keyPassword"] as String? // Make sure the path in key.properties (storeFile=app/upload-keystore.jks) // correctly points to your .jks file relative to the android/ directory. // The project.file path below is relative to android/app/build.gradle.kts storeFile = keystoreProperties["storeFile"]?.let { project.file("../" + it) } storePassword = keystoreProperties["storePassword"] as String? } } } buildTypes { getByName("release") { // ... other release settings signingConfig = signingConfigs.getByName("release") } } }
(Double-check paths carefully! If
key.properties
hasstoreFile=/Users/me/myapp/android/app/mykey.jks
(absolute path), then usestoreFile = keystoreProperties["storeFile"]?.let { file(it) }
.)
3. Upload Your First App Bundle (AAB)
Build your app:
flutter build appbundle --release
In Google Play Console, for your app:
Testing > Internal testing
.Click "Create new release."
Under "App bundles," click "Upload" and select your
app-release.aab
file.Fill in a "Release name" (e.g.,
1.0.0-initial
) and "Release notes" (e.g.,First version for testing.
).Click "Save," then "Review release." You don't have to roll it out yet. This just unlocks features.
4. Create a Service Account & JSON Key (For RevenueCat to talk to Google)
This is a multi-step dance between Google Cloud Platform (GCP) and Google Play Console.
In GCP Console (console.cloud.google.com):
Select the GCP Project linked to your Play Console account (or create/select a dedicated one for this app).
Go to
IAM & Admin > Service Accounts
.Click
+ CREATE SERVICE ACCOUNT
.Name: e.g.,
revenuecat-myapp-service
.Description: Optional. Click "CREATE AND CONTINUE."
Role: Select "Project" > "Viewer." Click "CONTINUE."
Skip step 3 ("Grant users access..."). Click "DONE."
Find your new service account in the list, click its email.
Go to the
KEYS
tab.ADD KEY > Create new key
. Choose JSON. Click "CREATE."A JSON file will download. Save it securely! This is super important.
In Google Play Console:
For your app:
Users and permissions
.Click "Invite new users."
Email: Paste the email of the service account you just created in GCP (it looks like
name@project-id.iam.gserviceaccount.com
).Permissions:
Go to the "Account permissions" tab (these are global for your developer account).
Grant the role "Finance" (this usually covers viewing financial data, orders, and subscriptions). If you prefer per-app permissions, explore the "App permissions" tab and grant "Finance" for your specific app. For simplicity now, global "Finance" is okay.
Click "Invite user."
5. Set Up Your Subscription Products
In Google Play Console, for your app:
Monetize > Products > Subscriptions
.Click "Create subscription."
Let's do "VIP Monthly" ($9.99) first as an example:
Product ID: A unique string (e.g.,
vip_monthly_android
or use the same as iOS:com.yourcompany.aivoicejournal.vip
.monthly
). SAVE THIS!Name: What users see (e.g.,
VIP Plan
).Click "Create."
Now you're editing this subscription. Click "Add base plan."
Base plan ID: e.g.,
vip-monthly-base
.Billing period: "Monthly."
(Other settings like grace period can be default). Click "Save."
Set Price for the Base Plan:
Find your new base plan, click "Manage prices" or "Set price."
Set your price (e.g., $9.99 for the US). You can set for all regions or adjust individually. "Save."
Activate the Base Plan: Find the "Activate" button for this base plan.
Back on the main edit page for the "VIP Monthly" subscription, ensure all is good and click "Save" for the whole subscription, then "Activate" it.
Now, repeat the "Create subscription" process (including adding and activating a base plan with a price) for:
Premium Monthly Android (e.g., Product ID:
premium_monthly_android
or same as iOS, $0.99, Monthly)Premium Annual Android (e.g., Product ID:
premium_annual_android
or same as iOS, $9.99, Annual)VIP Annual Android (e.g., Product ID:
vip_annual_android
or same as iOS, $99.99, Annual)
Make sure all 4 subscriptions (and their base plans) are Active.
Store setup done for Android! High five! π
Part 2: RevenueCat Magic - Connecting the Dots β¨
This is where RevenueCat makes our lives easier.
1. Add Platforms to Your RevenueCat Project
Go to your app project in RevenueCat.
Under your app name, click
+ Add
next to "Apps" or find "Project settings" and add platforms.Add "App Store (iOS)":
App Bundle ID: The one from Part 1, Section A, Step 1.
App Store Connect App-Specific Shared Secret: The one from Part 1, Section A, Step 3.
For "In-app purchase key configuration" and "App Store Connect API":
Upload the .p8 file (Part 1, Section A, Step 4).
Enter the Key ID (Part 1, Section A, Step 4).
Enter the Issuer ID (Part 1, Section A, Step 4).
Save.
Add "Play Store (Android)":
Application Package Name: Your
applicationId
fromandroid/app/build.gradle.kts
.Service Account Credentials JSON: Upload the JSON file you downloaded from GCP (Part 1, Section B, Step 4).
Save.
You should see green lights or success messages if connections are good!
2. Create Entitlements (What users get)
In RevenueCat:
Product catalog > Entitlements
.Click
+ New
.Identifier:
premium
Description: (Optional) e.g.,
Access to all premium features
.Click "Add."
Click
+ New
again.Identifier:
vip
Description: (Optional) e.g.,
Access to VIP perks and all premium features
.Click "Add."
3. Add Your Store Products to RevenueCat
In RevenueCat:
Product catalog > Products
.You can click
+ New
and add each product manually OR try the "Import" feature.If adding manually (repeat for all 8 products - 4 iOS, 4 Android):
Click
+ New
.Identifier: The EXACT Product ID from App Store Connect or Google Play Console (e.g.,
com.yourcompany.aivoicejournal.vip
.monthly
).Store: Select "App Store" or "Play Store" accordingly.
The "Store Product ID" field should auto-fill.
Click "Create."
On the product's detail page in RevenueCat, go to "Associated Entitlements."
For Premium products (monthly/annual, iOS/Android): Attach the
premium
entitlement.For VIP products (monthly/annual, iOS/Android): Attach the
vip
entitlement.
Save.
If "Import" works, great! Just double-check the entitlements are correctly associated after import.
4. Create an Offering and Packages (What users see)
This is how you group your products to display them in your app. We'll use one "default" offering.
In RevenueCat:
Product catalog > Offerings
.If there isn't one called
default
, click+ New
and create it (Identifier:default
, Display Name: e.g.,Standard Plans
).Click on your
default
offering to edit it. Go to the "Packages" tab.We need 4 packages because we have 4 combinations of (Premium/VIP) and (Monthly/Annual). We'll use Custom identifiers for our packages.
Package 1: Premium Monthly
Click
+ New Package
.Identifier (Dropdown): Choose "Custom."
Identifier (Text field):
premium_monthly
Description:
Premium - Monthly
Products:
For AiVoiceJournal (App Store): Select your "Premium Monthly iOS" product.
For AiVoiceJournal (Play Store): Select your "Premium Monthly Android" product.
Click "Add."
Package 2: VIP Monthly
Click
+ New Package
.Identifier (Dropdown): "Custom."
Identifier (Text field):
vip_monthly
Description:
VIP - Monthly
Products:
For App Store: Select "VIP Monthly iOS."
For Play Store: Select "VIP Monthly Android."
Click "Add."
Package 3: Premium Annual
Click
+ New Package
.Identifier (Dropdown): "Custom."
Identifier (Text field):
premium_annual
Description:
Premium - Annual
Products:
For App Store: Select "Premium Annual iOS."
For Play Store: Select "Premium Annual Android."
Click "Add."
Package 4: VIP Annual
Click
+ New Package
.Identifier (Dropdown): "Custom."
Identifier (Text field):
vip_annual
Description:
VIP - Annual
Products:
For App Store: Select "VIP Annual iOS."
For Play Store: Select "VIP Annual Android."
Click "Add."
Save your Offering if needed.
You should now have one "default" Offering with four Packages, each linked to the correct iOS and Android products! This was a marathon, but you're a champ! π
Part 3: Flutter Power-Up - Time to Code! π¨βπ»π©βπ»
1. Add purchases_flutter
SDK
In your
pubspec.yaml
:dependencies: flutter: sdk: flutter purchases_flutter: ^LATEST_VERSION # Check pub.dev for the latest
Run
flutter pub get
.
2. Get RevenueCat SDK API Keys
In RevenueCat:
Project settings > API keys
.Under "SDK API keys," copy the key for App Store (iOS) and the key for Play Store (Android).
3. Initialize RevenueCat in main.dart
import 'dart:io' show Platform; // For checking platform
import 'package:flutter/foundation.dart' show kIsWeb; // Optional, if you might support web
import 'package:purchases_flutter/purchases_flutter.dart';
// It's good practice to store keys securely, not directly in code for production.
// For this example, we'll show direct usage.
const String IOS_API_KEY = "your_ios_sdk_api_key_here";
const String ANDROID_API_KEY = "your_android_sdk_api_key_here";
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Purchases.setLogLevel(LogLevel.debug); // Helpful for development
PurchasesConfiguration configuration;
if (kIsWeb) {
// Handle web if you need to, or skip
print("RevenueCat Web configuration not set up.");
runApp(MyApp()); // Or your app's main widget
return;
} else if (Platform.isAndroid) {
configuration = PurchasesConfiguration(ANDROID_API_KEY);
} else if (Platform.isIOS) {
configuration = PurchasesConfiguration(IOS_API_KEY);
} else {
print("Unsupported platform for purchases.");
runApp(MyApp());
return;
}
await Purchases.configure(configuration);
runApp(MyApp());
}
(Replace placeholder keys with your actual keys!)
4. Fetching and Displaying Your Packages
Somewhere in your app (e.g., in a service or a ViewModel for your paywall screen):
Future<List<Package>> getSubscriptionPackages() async {
try {
Offerings offerings = await Purchases.getOfferings();
// Assuming your offering is 'default' and it's the current one
if (offerings.current != null && offerings.current!.availablePackages.isNotEmpty) {
// offerings.current.availablePackages will contain:
// - Package with identifier "premium_monthly"
// - Package with identifier "vip_monthly"
// - Package with identifier "premium_annual"
// - Package with identifier "vip_annual"
return offerings.current!.availablePackages;
}
} catch (e) {
print("Error fetching packages: $e");
}
return [];
}
// In your UI, you'd call this and then build widgets for each Package:
// For each package:
// Text(package.storeProduct.title)
// Text(package.storeProduct.description)
// Text(package.storeProduct.priceString)
// ElevatedButton(onPressed: () => purchasePackage(package), child: Text("Subscribe"))
5. Making a Purchase
Future<bool> purchasePackage(Package packageToPurchase) async {
try {
CustomerInfo customerInfo = await Purchases.purchasePackage(packageToPurchase);
// Check entitlements after purchase
bool isPremium = customerInfo.entitlements.all["premium"]?.isActive == true;
bool isVip = customerInfo.entitlements.all["vip"]?.isActive == true;
if (isVip || isPremium) {
print("Purchase successful! VIP: $isVip, Premium: $isPremium");
// Unlock features, navigate user, etc.
return true;
}
} on PlatformException catch (e) {
var errorCode = PurchasesErrorHelper.getErrorCode(e);
if (errorCode != PurchasesErrorCode.purchaseCancelledError) {
print("Purchase failed: ${e.message}");
// Show error to user
} else {
print("Purchase cancelled by user.");
}
} catch (e) {
print("Unexpected purchase error: $e");
}
return false;
}
6. Checking Entitlements (The "Are they a VIP/Premium?" check)
You'll need a way to check a user's status throughout your app.
// This can be a global state, a provider, a stream, etc.
// For simplicity:
bool currentUserIsPremium = false;
bool currentUserIsVip = false;
Future<void> updateUserSubscriptionStatus() async {
try {
CustomerInfo customerInfo = await Purchases.getCustomerInfo();
currentUserIsPremium = customerInfo.entitlements.all["premium"]?.isActive == true;
currentUserIsVip = customerInfo.entitlements.all["vip"]?.isActive == true;
if (currentUserIsVip) {
print("User is VIP (and thus also Premium)");
} else if (currentUserIsPremium) {
print("User is Premium");
} else {
print("User is on a free plan");
}
// Update your app's UI/state based on these flags
} catch (e) {
print("Error getting customer info: $e");
}
}
// Call updateUserSubscriptionStatus() when the app starts, after login, after purchase.
// Also, listen for updates:
// Purchases.addCustomerInfoUpdateListener((info) {
// updateUserSubscriptionStatus(); // Or directly use the 'info' object
// });
Remember: If a user is VIP, they typically get all Premium benefits too!
7. User Identification (Important for syncing purchases across devices!)
When a user logs into your app:
Future<void> identifyUserInRevenueCat(String yourAppUserId) async { try { await Purchases.logIn(yourAppUserId); updateUserSubscriptionStatus(); // Refresh entitlements } catch (e) { print("RevenueCat login error: $e"); } }
When a user logs out:
Future<void> logoutUserFromRevenueCat() async { try { await Purchases.logOut(); updateUserSubscriptionStatus(); // Entitlements will now be for an anonymous user } catch (e) { print("RevenueCat logout error: $e"); } }
Part 4: Testing - The Moment of Truth! π€
A. iOS Sandbox Testing
In App Store Connect:
Users and Access > Sandbox > Testers
. Add a test Apple ID.On a physical iOS device:
Settings > App Store
. Sign out of your regular Apple ID. Sign in with your Sandbox Tester Apple ID.Run your app from Xcode or
flutter run
onto the device.Try to purchase. You should see
[Environment: Sandbox]
in the purchase dialog. It won't charge you.
B. Android Testing
In Google Play Console:
Setup > License testing
. Add Gmail accounts that will be testers.Upload your app to a test track: Your signed AAB (from Part 1, B, Step 3) needs to be live on "Internal testing," "Closed testing," or "Open testing." Products often don't work with local debug builds.
On a physical Android device:
Sign in to the Google Play Store with one of your tester Gmail accounts as the primary account.
Install the app FROM THE PLAY STORE using the test track link (e.g., the opt-in link for Internal Testing). Don't run the version from
flutter run
.
Try to purchase. You should see test purchase indicators (e.g., "Test card, always approves"). No real money involved.
Check during testing:
Are all 4 plans (Prem/VIP, Mo/Yr) displayed correctly with correct prices?
Does the purchase dialog appear?
After "buying," does
updateUserSubscriptionStatus()
correctly reflect thepremium
orvip
entitlement?If you log out and log in with the same user on another device (or after reinstall), are purchases restored? (RevenueCat's
logIn
helps with this).
You Did It! ππ₯³
If you've made it this far and your tests are green, CONGRATULATIONS! You've successfully navigated the maze of in-app subscriptions for both iOS and Android. That's a massive achievement!
What's Next?
Build a beautiful paywall screen!
Add a "Restore Purchases" button (good practice, required by Apple).
Dive deeper into RevenueCat's analytics.
Explore advanced features like promotional offers or server-side receipt validation if you need them.
But for now, give yourself a pat on the back. You've unlocked a new level in app development! Happy coding, and may the subscriptions roll in! πΈ
Subscribe to my newsletter
Read articles from Adam Smaka directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
