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

Adam SmakaAdam Smaka
16 min read

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!):

  1. Store Setup (The Boring but Necessary Stuff): Getting your products ready on the App Store (iOS) and Google Play Store (Android).

  2. RevenueCat Magic ✨: Connecting the stores to RevenueCat, defining what users get (Entitlements), and setting up your "for sale" signs (Offerings & Packages).

  3. Flutter Power-Up πŸ‘¨β€πŸ’»πŸ‘©β€πŸ’»: Integrating the RevenueCat SDK into your app to show plans, handle purchases, and check who gets the cool features.

  4. 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:

  1. Apple Developer Account: If you're targeting iOS, you need this. Sign up at developer.apple.com/programs. Yes, there's an annual fee.

  2. Google Play Console Account: For Android, this is your mothership. Sign up at play.google.com/console/developers. There's a one-time fee.

  3. RevenueCat Account: This is our subscription superhero. Grab a free account at revenuecat.com.

  4. 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 (or Integrations > 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., inside android/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 in android/app/. Add this file to .gitignore!)

  • Configure android/app/build.gradle.kts (or build.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 has storeFile=/Users/me/myapp/android/app/mykey.jks (absolute path), then use storeFile = 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 from android/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 the premium or vip 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! πŸ’Έ

0
Subscribe to my newsletter

Read articles from Adam Smaka directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Adam Smaka
Adam Smaka