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
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...]
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>
(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();
}
}
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();
}
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();
}
Two huge wins fall out of this:
-
A mock implementation for Windows. MAUI development on Windows is fast — but there's no Play Store on your dev box. Register a
MockSubscriptionServicefor 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
-
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)
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
}
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
- Project → Android app (package name) + iOS app (bundle id) → copy the public API keys.
- Create the store products (Play Console / App Store Connect).
- RevenueCat: create an entitlement (
premium), attach products, build thedefaultoffering.
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)