DEV Community

Steve Dornan
Steve Dornan

Posted on

.NET MAUI In-App Subscriptions with RevenueCat: The 2026 Setup That Actually Compiles

If you've searched "RevenueCat MAUI" you've already discovered the awkward truth: RevenueCat has no official .NET MAUI SDK. Flutter, React Native, Unity, Cordova, KMP — all officially supported. MAUI? A community wrapper and a forum thread from 2023 asking when it's coming.

Here's the good news: the community answer is actually solid, and in this guide we'll go from File > New Project to a working subscription purchase on Android — including the two build errors that stop most people, and how to structure the code so your app never depends on any particular billing SDK.

The lay of the land

Your realistic options for MAUI subscriptions in 2026:

Option Verdict
Plugin.InAppBilling Direct store billing. Works, but you get raw store APIs — receipt validation, cross-platform entitlement logic, and grace-period handling are all yours to build.
Kebechet.Maui.RevenueCat.InAppBilling Community wrapper around RevenueCat's native SDKs. Actively maintained (v7.x, updated mid-2026), unified C# API, and you inherit RevenueCat's server-side receipt validation, entitlements, and analytics.
Raw StoreKit 2 / Play Billing bindings You are now a billing infrastructure company.

We're using the Kebechet wrapper. RevenueCat's free tier covers you to $2,500/month tracked revenue, which is exactly the phase where you shouldn't be building billing infrastructure.

Step 1 — Install

dotnet add package Kebechet.Maui.RevenueCat.InAppBilling
Enter fullscreen mode Exit fullscreen mode

Step 2 — The two build errors everyone hits

Error 1: AMM0000 manifest merger failure. The moment you add the package, your Android build explodes with walls of "Namespace 'com.android.billingclient' is used in multiple modules" warnings and, buried at the bottom, the actual killer:

uses-sdk:minSdkVersion 21 cannot be smaller than version 23
declared in library [androidx.lifecycle...]
Enter fullscreen mode Exit fullscreen mode

The MAUI template defaults to Android API 21. The Play Billing dependency chain requires more. The fix is one line in your .csproj:

<SupportedOSPlatformVersion
    Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">
    24.0
</SupportedOSPlatformVersion>
Enter fullscreen mode Exit fullscreen mode

(24 rather than 23 keeps you clear of the whole chain and covers 97%+ of active devices.) The namespace warnings are noise — once the minSdk error is gone, the build passes. I wrote a separate deep-dive on this error [link to article 2].

Error 2: IHttpClientFactory not found — not from this package, but you'll hit it the moment you wire your paywall to a backend. dotnet add package Microsoft.Extensions.Http.

Step 3 — Initialize (in the right place)

The SDK must initialize in OnStart, not in App's constructor:

public partial class App : Application
{
    private readonly ISubscriptionService _subscriptions;

    public App(ISubscriptionService subscriptions)
    {
        InitializeComponent();
        _subscriptions = subscriptions;
    }

    protected override void OnStart()
    {
        _subscriptions.Initialize();   // wraps Purchases.Configure
        base.OnStart();
    }
}
Enter fullscreen mode Exit fullscreen mode

Notice we're already hiding RevenueCat behind our own interface. That's the single most important decision in this article — more below.

Step 4 — Don't marry the SDK: the ISubscriptionService abstraction

The wrapper's IRevenueCatBilling interface is fine, but binding your ViewModels to it means every SDK change ripples through your app. Define the interface your app actually needs:

public interface ISubscriptionService
{
    void Initialize();
    Task LoginAsync(Guid userId);
    Task LogoutAsync();
    Task<SubscriptionState> GetStateAsync(bool forceRefresh = false);
    Task<bool> IsPremiumAsync();
    Task<IReadOnlyList<StorePackage>> GetPackagesAsync(bool forceRefresh = false);
    Task<PurchaseResult> PurchaseAsync(StorePackage package);
    Task<bool> RestoreAsync();
}
Enter fullscreen mode Exit fullscreen mode

Then the RevenueCat-backed implementation maps their DTOs to yours:

public async Task<IReadOnlyList<StorePackage>> GetPackagesAsync(bool forceRefresh = false)
{
    var offerings = await _revenueCat.GetOfferings(forceRefresh);
    return offerings
        .SelectMany(o => o.AvailablePackages)
        .Select(p => new StorePackage
        {
            Identifier = p.Identifier,
            Title = p.Product.Sku,
            PriceLocalized = p.Product.Pricing.PriceLocalized,
            Period = p.Product.SubscriptionPeriod?.Unit.ToString(),
            NativePackage = p            // opaque handle for purchase time
        })
        .ToList();
}
Enter fullscreen mode Exit fullscreen mode

Two huge wins fall out of this:

  1. A mock implementation for Windows. MAUI development on Windows is fast — but there's no Play Store on your dev box. Register a MockSubscriptionService for non-mobile targets and you can build and demo your entire paywall flow without an emulator or a store account:
#if ANDROID || IOS
    builder.Services.AddRevenueCatBilling();
    builder.Services.AddSingleton<ISubscriptionService, RevenueCatSubscriptionService>();
#else
    builder.Services.AddSingleton<ISubscriptionService, MockSubscriptionService>();
#endif
Enter fullscreen mode Exit fullscreen mode
  1. Web payments slot in invisibly. When you later add Stripe checkout for web purchases (post-Epic external links — [link to article 3]), GetStateAsync() merges both sources and your feature gates never change.

Step 5 — Identify the user

Anonymous purchases are a support nightmare — the moment a user reinstalls or switches devices, "where's my subscription?" tickets begin. After your login flow succeeds:

await _subscriptions.LoginAsync(userId);   // RevenueCat Login(appUserId)
Enter fullscreen mode Exit fullscreen mode

Now entitlements follow the account, not the device.

Step 6 — Gate features

[RelayCommand]
private async Task UsePremiumFeatureAsync()
{
    if (!await _subscriptions.IsPremiumAsync())
    {
        await _nav.GoToPaywallAsync();
        return;
    }
    // premium feature
}
Enter fullscreen mode Exit fullscreen mode

GetStateAsync checks the RevenueCat entitlement (cached ~5 min) and falls back to your server for web/trial subscriptions. One code path, every payment source.

Step 7 — RevenueCat dashboard wiring

  1. Project → Android app (package name) + iOS app (bundle id) → copy the public API keys.
  2. Create the store products (Play Console / App Store Connect).
  3. RevenueCat: create an entitlement (premium), attach products, build the default offering.

The offering is why this beats hardcoding SKUs: your paywall renders whatever the offering contains, so you can test pricing without shipping an app update.

Testing on Android

Upload one build to a Closed Testing track, add yourself as a license tester, and purchases on debug builds use test cards. No real money, real purchase flow.


Or skip all of it

Everything in this article — plus the auth system (OTP, JWT, rotating refresh tokens), the Stripe web-checkout path, server-issued free trials, and a finished paywall UI — is packaged as MidasKit, a production-grade .NET MAUI subscription boilerplate. It compiles out of the box; you wire your keys and ship. Get it here.

Top comments (0)