subscription_guard

A declarative, provider-agnostic subscription tier gating package for Flutter.

pub package pub points likes popularity license flutter

Stop writing if/else for every premium feature. Gate any widget, route, or feature with one line of code.


The Problem

// This is scattered across your entire codebase ๐Ÿ˜ฉ
if (user.subscription == 'pro' || user.subscription == 'premium') {
  return AdvancedStatsWidget();
} else {
  return UpgradePrompt();
}
// And again here... and in your routes... and in your navigation...
// 35+ features? That's 35+ if/else blocks. Good luck maintaining that.

The Solution

// One line. Done. โœ…
SubscriptionGuard(
  requiredTier: 'pro',
  child: AdvancedStatsWidget(),
)

โœจ Features

  • ๐Ÿ›ก๏ธ Declarative Gating โ€” SubscriptionGuard widget for tier, feature, or specific-tier gating
  • ๐ŸŽจ 4 Guard Behaviors โ€” Hide, Disable, Blur, or Replace locked content
  • ๐Ÿงญ Route Protection โ€” Guard entire screens with navigation helpers
  • โฑ๏ธ Trial Support โ€” Built-in trial countdown with TrialBanner
  • ๐Ÿ“Š Analytics Callbacks โ€” Track which features users hit paywalls on
  • ๐Ÿ”Œ Provider Agnostic โ€” Works with RevenueCat, Adapty, or any purchase SDK
  • ๐Ÿ› Debug Overlay โ€” Draggable tier switcher for testing during development
  • ๐Ÿ“ฆ Zero Dependencies โ€” Pure Flutter, no external packages
  • ๐ŸŽฏ Feature Registry โ€” Map features to tiers centrally, reference by ID
  • ๐Ÿ’ก Programmatic Access โ€” Check tier access anywhere in code

๐Ÿ“ฆ Installation

Add to your pubspec.yaml:

dependencies:
  subscription_guard: ^0.0.1

Then run:

flutter pub get

๐Ÿš€ Quick Start

Step 1: Define your tiers

final config = SubscriptionConfig(
  tiers: [
    Tier(id: 'free', level: 0, label: 'Free'),
    Tier(id: 'pro', level: 1, label: 'Pro'),
    Tier(id: 'premium', level: 2, label: 'Premium'),
  ],
  features: {
    'advanced_stats': 'pro',
    'export_pdf': 'pro',
    'team_management': 'premium',
  },
);

Step 2: Wrap your app

SubscriptionGuardProvider(
  config: config,
  currentTier: 'free', // Update this when user purchases
  onUpgradeRequested: (requiredTier) {
    // Show your paywall (RevenueCat, Adapty, etc.)
    showPaywall(requiredTier);
  },
  child: MyApp(),
)

Step 3: Guard any widget

SubscriptionGuard(
  requiredTier: 'pro',
  child: AdvancedStatsWidget(),
)

That's it! The widget automatically shows or hides based on the user's tier. When the tier changes, all guards update instantly.


๐Ÿ“š Usage

Guard Behaviors

// Hide โ€” completely removes from widget tree
SubscriptionGuard(requiredTier: 'pro', behavior: GuardBehavior.hide, child: ProWidget())

// Disable โ€” visible but greyed out and non-interactive
SubscriptionGuard(requiredTier: 'pro', behavior: GuardBehavior.disable, child: ProWidget())

// Replace โ€” shows locked UI (default behavior)
SubscriptionGuard(requiredTier: 'pro', behavior: GuardBehavior.replace, child: ProWidget())

// Blur โ€” shows blurred preview with adaptive lock overlay
SubscriptionGuard(requiredTier: 'pro', behavior: GuardBehavior.blur, child: ProWidget())

Feature-Based Gating

// Define features in config, reference by ID
SubscriptionGuard.feature(
  featureId: 'export_pdf',
  child: ExportButton(),
)

Specific Tier Gating (Non-Hierarchical)

// Only 'pro' users โ€” NOT premium, NOT free
SubscriptionGuard.allowedTiers(
  tierIds: ['pro'],
  child: ProExclusiveBadge(),
)

Custom Locked Widget

SubscriptionGuard(
  requiredTier: 'premium',
  lockedBuilder: (context, requiredTier, currentTier) {
    return MyCustomPaywall(tier: requiredTier);
  },
  child: PremiumFeature(),
)
// Method 1: pushGuarded โ€” blocks navigation if tier insufficient
SubscriptionRouteGuard.pushGuarded(
  context,
  requiredTier: 'pro',
  route: MaterialPageRoute(builder: (_) => AnalyticsScreen()),
  onBlocked: (required, current) => showUpgradeDialog(required),
);

// Method 2: SubscriptionPageRoute โ€” always pushes, shows locked UI if blocked
Navigator.of(context).push(
  SubscriptionPageRoute(
    requiredTier: 'pro',
    builder: (_) => AnalyticsScreen(),
  ),
);

// Method 3: GoRouter compatible redirect (no go_router dependency required)
GoRoute(
  path: '/analytics',
  redirect: subscriptionRedirect(
    requiredTier: 'pro',
    redirectPath: '/upgrade',
  ),
)

Programmatic Access Check

final scope = SubscriptionGuardScope.of(context);

if (scope.hasAccess('pro')) {
  // Do something for pro users
}

if (scope.hasFeatureAccess('export_pdf')) {
  // Feature is accessible
}

// Or use the route guard utility
final result = SubscriptionRouteGuard.checkAccess(context, requiredTier: 'pro');
if (result.hasAccess) { /* ... */ }

Trial Support

SubscriptionGuardProvider(
  config: config,
  currentTier: 'pro',
  trialInfo: TrialInfo(
    isTrialing: true,
    endsAt: DateTime.now().add(Duration(days: 7)),
  ),
  child: Column(
    children: [
      TrialBanner(onTap: () => showUpgradeDialog()),
      SubscriptionGuard(
        requiredTier: 'pro',
        allowDuringTrial: true, // default
        child: ProFeature(),
      ),
    ],
  ),
)

Analytics / Tracking

SubscriptionGuardProvider(
  config: config,
  currentTier: 'free',
  onFeatureBlocked: (featureId, requiredTier, currentTier) {
    analytics.track('feature_gated', {
      'feature': featureId,
      'required_tier': requiredTier.id,
      'current_tier': currentTier.id,
    });
  },
  onUpgradeRequested: (requiredTier) {
    analytics.track('upgrade_requested', {'tier': requiredTier.id});
  },
  child: MyApp(),
)

Debug Overlay

SubscriptionGuardDebugOverlay(
  enabled: kDebugMode, // Auto-disabled in release builds
  onTierChanged: (tierId) {
    setState(() => _currentTier = tierId);
  },
  child: MyApp(),
)

๐Ÿ”Œ Works With Any Purchase SDK

subscription_guard doesn't handle purchases โ€” it only handles UI gating. Pair it with your preferred purchase SDK:

RevenueCat:

Purchases.addCustomerInfoUpdateListener((info) {
  final tier = info.entitlements.active.containsKey('premium')
      ? 'premium'
      : info.entitlements.active.containsKey('pro') ? 'pro' : 'free';
  setState(() => _currentTier = tier);
});

Adapty:

Adapty.getProfile().then((profile) {
  final tier = profile.accessLevels['premium']?.isActive == true
      ? 'premium' : 'free';
  setState(() => _currentTier = tier);
});

Raw in_app_purchase:

InAppPurchase.instance.purchaseStream.listen((purchases) {
  setState(() => _currentTier = deriveTierFromPurchases(purchases));
});

Just update currentTier on the provider โ€” subscription_guard handles the rest.


๐Ÿ—๏ธ Architecture

subscription_guard uses a pure InheritedWidget architecture โ€” no BLoC, Riverpod, or Provider dependency. Your purchase SDK feeds the current tier into SubscriptionGuardProvider, which propagates it down the widget tree. Every SubscriptionGuard, SubscriptionRouteGuard, and TrialBanner reacts automatically when the tier changes.

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚              Your Purchase SDK                    โ”‚
โ”‚         (RevenueCat / Adapty / etc.)              โ”‚
โ”‚                      โ”‚                            โ”‚
โ”‚                      โ–ผ                            โ”‚
โ”‚      SubscriptionGuardProvider(currentTier)       โ”‚
โ”‚                      โ”‚                            โ”‚
โ”‚           โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                 โ”‚
โ”‚           โ–ผ          โ–ผ          โ–ผ                 โ”‚
โ”‚   SubscriptionGuard  RouteGuard  TrialBanner      โ”‚
โ”‚           โ”‚          โ”‚          โ”‚                 โ”‚
โ”‚      โ”Œโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”   Access    Countdown            โ”‚
โ”‚      โ”‚ Access? โ”‚   Check                          โ”‚
โ”‚      โ”œโ”€Yesโ”€โ–บShowโ”‚                                 โ”‚
โ”‚      โ””โ”€Noโ”€โ”€โ–บLockโ”‚                                 โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

๐Ÿ“– API Reference

Full API documentation is available at: pub.dev/documentation/subscription_guard

Key Classes

Class Description
SubscriptionGuardProvider Root widget that provides tier state to the tree
SubscriptionGuard Core gating widget โ€” tier, feature, or allowedTiers
SubscriptionConfig Configuration for tiers and feature mappings
Tier Represents a single subscription tier
GuardBehavior Enum: hide, disable, replace, blur
TrialInfo Trial period state
TrialBanner Trial countdown banner widget
DefaultLockedWidget Built-in locked state UI
SubscriptionRouteGuard Static navigation guard utilities
SubscriptionPageRoute MaterialPageRoute with built-in tier check
RouteAccessResult Result of a programmatic access check
SubscriptionGuardDebugOverlay Debug tier switcher overlay
SubscriptionGuardScope Programmatic access to subscription state

๐ŸŽฎ Example

Check out the example app for a complete demo of all features.

cd example
flutter create .  # Generate platform files (first time only)
flutter run

The example app includes:

  • Tier switching simulation
  • All 4 guard behaviors
  • Feature-based gating
  • Navigation guards
  • Trial banner
  • Debug overlay
  • Custom locked builders
  • Analytics callback logging

๐Ÿค Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'feat: add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

Please make sure to:

  • Update tests as appropriate
  • Follow existing code style
  • Add documentation for new features
  • Run dart analyze and flutter test before submitting

๐Ÿ“„ License

This project is licensed under the MIT License โ€” see the LICENSE file for details.


Built with โค๏ธ by YOUR_NAME

If this package helps you, please โญ the repo and ๐Ÿ‘ on pub.dev!

Libraries

subscription_guard
A declarative, provider-agnostic subscription tier gating package for Flutter.