bosc_revenuecat_package (1.0.1)
Installation
dart pub add bosc_revenuecat_package:1.0.1 --hosted-url=About this package
RevenueCat Subscription
A simplified, modern Flutter package for seamless RevenueCat integration. Focus on your UI—everything else (purchases, status checks, real-time updates) is handled. Covers 100% of common use cases with clean APIs and no custom models.
Compatible with RevenueCat SDK v9.10.0+ (as of Dec 2025). Built on official RevenueCat Flutter SDK.
Why This Package?
RevenueCat abstracts StoreKit (iOS) and Google Play Billing (Android) complexities like receipts, entitlements, and cross-platform sync. This wrapper adds:
- Widget for Paywalls: Auto-manages state, loading, errors, and eligibility.
- Global Client: App-wide checks without reinitializing.
- Best Practices: Logging, fresh/cached fetches, iOS-only guards (e.g., trials).
- No Boilerplate: Direct SDK types; just design your UI.
Ideal for freemium apps, ad-free upgrades, or premium features.
Features
| Feature | Description |
|---|---|
| Easy Setup | One-time config with iOS/Android keys. |
| Purchases | Buy packages/products; handles cancellations gracefully. |
| Status Checks | Active entitlements, packages, granular statuses (active/inactive/expired/billingIssue). |
| Trials/Intros | Eligibility detection for UI badges (iOS-only). |
| Real-Time | Global stream for updates (e.g., background buys). |
| User Mgmt | Login/logout, attributes, email/phone. |
| Tools | Restore/sync, manage URL, code redemption (iOS). |
| Widget | SubscriptionWidget with auto-state (offerings, eligibility, statuses). |
| App-Wide | Singleton-style client for any screen. |
| Debugging | Callbacks + SDK toggles. |
Exports SDK types like Offerings, Package, CustomerInfo.
Installation
-
Add Dependency:
dependencies: revenuecat_subscription: ^1.0.1Run
flutter pub get. -
Platform Setup (from RevenueCat Docs):
- iOS: Set deployment target ≥11.0 in
Podfile; enable In-App Purchase in Xcode Capabilities; use Swift ≥5.0. - Android: Add
<uses-permission android:name="com.android.vending.BILLING" />inAndroidManifest.xml; setlaunchMode="standard"or"singleTop"for your Activity. - Web (Beta): Use
purchases_flutter: ^9.0.0-beta.3(package pins latest).
- iOS: Set deployment target ≥11.0 in
-
RevenueCat Dashboard:
- Create public API keys (iOS:
appl_xxx, Android:goog_xxx). - Set up Products (e.g.,
monthly: $9.99,yearly: $99.99) and Entitlements (e.g.,premium). - Create Offerings (e.g., "default" with monthly/yearly packages).
- Create public API keys (iOS:
See RevenueCat Flutter Install for details.
Quick Start
1. Configure in main.dart
import 'package:flutter/material.dart';
import 'package:revenuecat_subscription/revenuecat_subscription.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Configure once
await RevenueCatClient.configure(
iosApiKey: 'appl_your_ios_key',
androidApiKey: 'goog_your_android_key',
logLevel: LogLevel.debug, // Optional
);
// Optional: Global logging
RevenueCatClient.onLog = (msg) => debugPrint('[RevenueCat] $msg');
RevenueCatClient.onError = (err, stack) => debugPrint('Error: $err');
runApp(MyApp());
}
2. Use Widget for Paywalls (Easiest)
Wrap your paywall screen:
// paywall_screen.dart
import 'package:flutter/material.dart';
import 'package:revenuecat_subscription/revenuecat_subscription.dart';
class PaywallScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: SubscriptionWidget(
userId: 'optional_user_123', // For identified users
entitlementId: 'premium', // Optional: Specific check
onSubscribed: () => Navigator.pop(context), // Success callback
builder: (context, state) {
if (state.isLoading) return const Center(child: CircularProgressIndicator());
if (state.error != null) return Center(child: Text('Error: ${state.error}'));
if (state.hasActiveSubscription) {
return const Center(child: Text('Premium Unlocked! 🎉'));
}
// Paywall UI
final offering = state.offerings?.current;
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Unlock Premium', style: TextStyle(fontSize: 24)),
const SizedBox(height: 20),
// Packages with trial badges & status
...?offering?.availablePackages.map((pkg) {
final eligible = state.eligibility?[pkg.storeProduct.identifier] == IntroEligibility.introPriceOrTrial;
final status = state.subscriptionStatuses?[pkg.storeProduct.identifier] ?? SubscriptionStatus.inactive;
return Card(
child: ListTile(
title: Text(pkg.storeProduct.title),
subtitle: Text('${pkg.storeProduct.priceString} - Status: $status'),
trailing: eligible ? const Icon(Icons.local_offer, color: Colors.green) : null,
onTap: () => state.purchasePackage(pkg),
),
);
}),
const SizedBox(height: 20),
ElevatedButton(
onPressed: state.restorePurchases,
child: const Text('Restore Purchases'),
),
Text(offering?.metadata['footer'] ?? 'Terms apply.'), // From dashboard
],
);
},
),
);
}
}
- How it Works: Auto-fetches offerings/customerInfo, listens for updates, shows eligibility badges (iOS), and handles purchases/restores.
3. Use Client Directly (App-Wide)
For checks anywhere (e.g., nav guards):
// global_revenuecat.dart
final globalClient = RevenueCatClient(); // Singleton pattern
// In a screen (e.g., home.dart)
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder<bool>(
future: globalClient.hasActiveEntitlement('premium'),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data == true) {
return const PremiumDashboard(); // Premium UI
}
return Column(
children: [
const Text('Free Tier'),
ElevatedButton(
onPressed: () async {
final offerings = await globalClient.getOfferings();
final pkg = offerings.current?.availablePackages.first;
if (pkg != null) {
try {
await globalClient.purchasePackage(pkg);
// Handle success (e.g., refresh UI)
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Purchase failed: $e')));
}
}
},
child: const Text('Upgrade to Premium'),
),
],
);
},
),
);
}
}
Usage Examples & Use Cases
Use Case 1: Freemium App (Basic Subscriptions)
- Scenario: Free app with premium features (e.g., ad-free, unlimited access).
- Implementation:
- Use
SubscriptionWidgetin a paywall modal. - Check
hasActiveEntitlement('premium')to gate features. - Example: In
main.dart, wrap routes with a guard:// Route to premium if subscribed if (await globalClient.hasActiveSubscription()) { Navigator.pushNamed(context, '/premium'); } else { Navigator.pushNamed(context, '/paywall'); }
- Use
- Pro Tip: Use
customerInfoStreamfor real-time unlocks after purchase.
Use Case 2: Multi-Tier Subscriptions (Entitlements)
- Scenario: Monthly ($4.99), Yearly ($49.99), Lifetime ($99) tiers.
- Implementation:
- Dashboard: Products → Entitlements (
pro,lifetime). - Code: Fetch statuses for tiers:
final statuses = await globalClient.getSubscriptionStatuses(['monthly', 'yearly']); if (statuses['yearly'] == SubscriptionStatus.active) { // Show yearly perks }
- Dashboard: Products → Entitlements (
- Example Paywall: Customize widget builder to highlight best value (e.g., yearly with trial badge).
Use Case 3: Trial-Driven Onboarding (iOS Focus)
- Scenario: Show "7-Day Free Trial" only for eligible users.
- Implementation:
- In widget:
state.eligibility?['monthly'] == IntroEligibility.introPriceOrTrial. - Code:
final eligibility = await globalClient.checkTrialOrIntroEligibility(['monthly']); if (eligibility['monthly'] == IntroEligibility.introPriceOrTrial) { // Badge: "Try Free for 7 Days" }
- In widget:
- Cross-Platform Note: Falls back gracefully on Android.
Use Case 4: User Analytics & Management
- Scenario: Track sign-ups, set custom attributes (e.g., source: 'google').
- Implementation:
// Login on auth await globalClient.logIn('user_123'); await globalClient.setAttributes({'plan': 'pro', 'source': 'organic'}); // Check user ID final userId = await globalClient.getAppUserId(); - Restore/Sync: Add "Restore" button; call
restorePurchases()orsyncPurchases()post-web buy.
Use Case 5: Ad-Free Upgrade (with AdMob)
- Scenario: Remove ads on subscribe (integrate with AdMob).
- Implementation (from RevenueCat Blog):
- On
hasActiveSubscription()true:MobileAds.instance.updateRequestConfiguration(...)to disable ads. - Example: Listen to stream:
globalClient.customerInfoStream.listen((info) { if (info.entitlements.active.isNotEmpty) { // Pause AdMob interstitials } });
- On
Advanced: Real-Time Updates
// In provider or init
globalClient.customerInfoStream.listen((info) {
final isPro = info.entitlements.all['premium']?.isActive ?? false;
// Update app state (e.g., Riverpod notifier)
});
Troubleshooting
| Issue | Solution |
|---|---|
| No Offerings | Verify dashboard products/entitlements match keys. Enable debug: setDebugLogsEnabled(true). |
| Purchase Fails | Check launchMode (Android); test sandbox. Logs via onError. |
| Trials Not Showing | iOS-only; ensure products have intro pricing in App Store Connect. |
| Cache Stale | Use getCustomerInfoFresh() or invalidateCustomerInfoCache(). |
| Web Support | Beta: Pin purchases_flutter: ^9.0.0-beta.3; use Stripe for web buys. |
Full logs: Set onLog/onError. Test on devices (emulators flaky for billing).
Contributing
- Fork, PR with tests (use
mockitofor mocks). - Run
dart analyze && flutter test. - Focus: Simplicity, cross-platform.
License
MIT. See LICENSE.
Inspired by RevenueCat Flutter Tutorial and Official Docs.