Mastering Flutter Billing: Beyond Stripe Payments to Robust Subscription Logic
The Flutter news you actually need
No spam, ever. Unsubscribe in one click.
So you’ve integrated Stripe (or another payment gateway) into your Flutter app. The payment sheet works beautifully, the checkout completes, and the money arrives. But now the real challenge begins: how do you reliably grant, update, and revoke access based on that payment? How do you handle subscription renewals, cancellations, and one-time credits without leaving users stranded or giving away paid features for free?
The hard part isn’t processing the payment—it’s building the robust logic that manages user entitlements after the payment succeeds. Let’s break down a practical approach.
The Core Problem: State Synchronization
Your payment gateway (e.g., Stripe) and your application backend live in different worlds. A user’s subscription status can change at any moment: a renewal succeeds, a payment fails, a user cancels, or a refund is issued. Your app needs to reflect these changes accurately and immediately.
The naive approach is to rely solely on webhook events from your payment provider to update your database. This is fragile. Webhooks can be delayed, sent out of order, or even fail to arrive. If your webhook handler crashes while processing an invoice.paid event, the user might never get their premium access.
A Robust Pattern: The Source of Truth Check
The solution is to treat webhooks as signals, not commands. When a webhook arrives, use it as a prompt to verify the current state with the payment gateway’s API, then update your system accordingly. This pattern ensures consistency even if webhooks are missed or processed incorrectly.
Here’s a conceptual flow:
- User completes a purchase in your Flutter app.
- Your backend creates a record (e.g.,
PurchaseIntentwith statusprocessing). - A webhook arrives (e.g.,
checkout.session.completed). - Your webhook handler fetches the latest subscription data directly from Stripe’s API using the ID from the webhook.
- Your backend reconciles this data with your user’s access record, updating their
isActive,planId,credits, etc. - Your Flutter app reflects this state, either via polling, WebSockets, or by fetching fresh user data on key app events.
Implementing the Reconciliation Logic
Let’s look at a simplified backend service (in Dart, suitable for a server-side Shelf or Dart Frog backend) that handles a subscription webhook. Notice how it calls the Stripe API to get the definitive status.
class SubscriptionService {
final StripeApi stripeApi;
final UserRepository userRepo;
Future<void> handleSubscriptionUpdated(String stripeSubscriptionId) async {
// 1. Fetch the true current state from the source of truth (Stripe)
final Subscription subscription = await stripeApi.fetchSubscription(stripeSubscriptionId);
// 2. Find the associated user in your database
// You should store the stripeSubscriptionId and/or stripeCustomerId in your user model.
final user = await userRepo.findByStripeSubscriptionId(stripeSubscriptionId);
if (user == null) {
// Log error: This should not happen if your data linking is set up correctly.
return;
}
// 3. Reconcile the status
final String newPlanId = subscription.items.data.first.price.id;
final bool isCurrentlyActive = subscription.status == 'active' || subscription.status == 'trialing';
// 4. Update your database with the verified state
await userRepo.updateSubscription(
userId: user.id,
isActive: isCurrentlyActive,
currentPlanId: newPlanId,
subscriptionPeriodEnd: DateTime.fromMillisecondsSinceEpoch(subscription.currentPeriodEnd * 1000),
);
// 5. Trigger any side effects (e.g., grant cloud credits, send welcome email)
if (isCurrentlyActive && user.currentPlanId != newPlanId) {
await grantPlanBenefits(user.id, newPlanId);
}
}
}
Flutter App State Management
In your Flutter app, you need a way to fetch and listen to this subscription state. Don’t store the access state only locally; always verify with your backend.
class UserAuthProvider with ChangeNotifier {
User? _currentUser;
SubscriptionInfo? _subscription;
Future<void> loadUserProfile() async {
// Fetch the latest user data, including subscription info, from your backend
final profile = await ApiService.fetchUserProfile();
_currentUser = profile.user;
_subscription = profile.subscription;
// If the UI depends on subscription access, notify listeners
notifyListeners();
}
bool get hasActivePremium => _subscription?.isActive == true && _subscription?.planId == 'premium_monthly';
// Call this on app start and after any purchase flow
Future<void> refreshSubscriptionStatus() async {
await loadUserProfile();
}
}
Call refreshSubscriptionStatus() at strategic points: on app startup, when the app resumes from the background, and after returning from your checkout flow.
Common Mistakes to Avoid
- Trusting Webhook Payloads Blindly: Never update your database based only on the webhook JSON. Use the IDs provided to fetch fresh data via API.
- Forgetting Edge Cases: What happens on a
past_duestatus? What about acanceledsubscription that is still within its paid period? Your logic must handle all subscription statuses. - Client-Side Only Checks: Never gate feature access solely on a boolean stored in
SharedPreferences. A determined user can alter this. Your backend must enforce all access rules. - Poor Data Linking: Ensure you reliably link Stripe objects (
customer.id,subscription.id) to your internal user IDs, often via metadata when creating the Checkout Session.
Handling Credits and Usage Tracking
For features like monthly renewable credits, your backend should manage the counter. Reset credits on the renewal date (which you get from the subscription’s current_period_end), and decrement them on use. Track usage in your database, not in the payment gateway.
Building robust subscription logic requires shifting your mindset. The payment is just the beginning. By using webhooks as triggers for a verification process and keeping your backend as the single manager of entitlements, you can create a billing system that is both reliable and scalable. This approach, while requiring more upfront work, saves endless support headaches and ensures your users have a seamless experience.
This blog is produced with the assistance of AI by a human editor. Learn more
Related Posts
Flutter's Hidden Gem: Building Powerful Linux Desktop Apps with Ease
Flutter is gaining traction as a robust framework for Linux desktop development, offering a smoother experience compared to traditional toolkits like GTK and Qt. This post will explore why Flutter excels for Linux apps, highlight its advantages, and provide practical insights for developers looking to leverage Flutter for their desktop projects.
Simplifying Flutter Desktop Deployment: Signing and Distribution for Windows
Deploying Flutter desktop apps on Windows can be challenging, especially regarding code signing and preventing Windows from blocking installations. This post will guide developers through the process of signing their Flutter desktop applications, exploring alternatives to the Microsoft Store like creating executable installers, and covering the necessary steps to ensure a smooth user experience.
Mastering Flutter Tooling: Streamlining SDK Management and Installation on Windows
Developers frequently voice frustrations about Flutter SDK version management and installation processes, particularly on Windows. This post will explore best practices for managing multiple Flutter versions using tools like FVM, discuss integrating Flutter with package managers like Winget for a smoother Windows setup, and offer strategies to ensure a consistent development environment across projects and teams.