A quick practice project I built to learn in-app subscription and payment handling in Flutter using RevenueCat.
I hadn't worked with payments in mobile apps before, so I built a simple news reader with a free/premium tier to get hands-on with the RevenueCat SDK — entitlements, offerings, paywalls, restore purchases, and real-time subscription state.
A content/news reader with two tiers:
- Free — 3 articles per day, general & tech categories
- Premium — unlimited articles, Science, Finance & Exclusive categories
- Free vs Premium feature gating — 3 free articles/day + locked premium categories
- Subscription status & management — real-time entitlement checks, renewal info
- Paywall screen — dynamically loads offerings from RevenueCat dashboard
- Restore purchases — required by App Store & Play Store guidelines
- Real-time listener — subscription state updates across the app instantly
flutter pub get- Create a free account at app.revenuecat.com
- Create a new project → add iOS and/or Android app
- Copy your API keys into
lib/services/revenue_cat_service.dart:static const String _appleApiKey = 'appl_YOUR_KEY_HERE'; static const String _googleApiKey = 'goog_YOUR_KEY_HERE';
- Go to Products → add your App Store / Play Store product IDs
- Go to Entitlements → create one called
premium - Attach your products to the
premiumentitlement - Go to Offerings → create a
defaultoffering with monthly + annual packages
Android (android/app/src/main/AndroidManifest.xml):
<uses-permission android:name="com.android.vending.BILLING" />iOS — No extra config needed. Make sure StoreKit is linked (it is by default).
flutter runTesting tip: Use RevenueCat's sandbox environment. On Android use a test account in Google Play Console. On iOS use a Sandbox Apple ID.
lib/
├── main.dart # RC initialization + app entry
├── services/
│ └── revenue_cat_service.dart # All RevenueCat SDK calls
├── cubit/
│ └── subscription_cubit.dart # Subscription state management
├── models/
│ └── article.dart # Article model + mock data
├── screens/
│ ├── home_screen.dart # Feed with gating logic
│ ├── paywall_screen.dart # Dynamic offerings paywall
│ ├── subscription_management_screen.dart
│ └── article_detail_screen.dart
└── widgets/
├── article_card.dart # Card with lock overlay
└── premium_banner.dart # Inline upgrade prompt
The app uses a single premium entitlement. Rather than checking product IDs directly,
entitlements abstract the subscription logic — so adding a new product tier never
requires an app update.
customerInfo.entitlements.active.containsKey('premium')Prices and packages are fetched from the RevenueCat dashboard at runtime. This means you can run A/B tests on pricing without releasing a new app version.
final offerings = await Purchases.getOfferings();
final packages = offerings.current!.availablePackages;The app registers a real-time listener so subscription state (e.g., a renewal or cancellation) is reflected across all screens immediately — no polling required.
Purchases.addCustomerInfoUpdateListener(_onCustomerInfoUpdated);Required by both App Store and Google Play guidelines. Allows users who reinstall the app, switch devices, or reinstall to recover their subscription.
await Purchases.restorePurchases();- Entitlements over product IDs — checking
entitlements.activeinstead of hardcoded product strings means you can add new plans without touching app code - Offerings are dashboard-driven — prices and packages load from RevenueCat at runtime, so you can adjust or A/B test pricing without a release
- CustomerInfo listener — subscribing to real-time updates means the UI reacts immediately to renewals or cancellations, no polling needed
- Restore purchases — this is required by both App Store and Play Store, easy to miss when you're first starting out