Kebechet.Maui.RevenueCat.InAppBilling
9.0.0
Prefix Reserved
dotnet add package Kebechet.Maui.RevenueCat.InAppBilling --version 9.0.0
NuGet\Install-Package Kebechet.Maui.RevenueCat.InAppBilling -Version 9.0.0
<PackageReference Include="Kebechet.Maui.RevenueCat.InAppBilling" Version="9.0.0" />
<PackageVersion Include="Kebechet.Maui.RevenueCat.InAppBilling" Version="9.0.0" />
<PackageReference Include="Kebechet.Maui.RevenueCat.InAppBilling" />
paket add Kebechet.Maui.RevenueCat.InAppBilling --version 9.0.0
#r "nuget: Kebechet.Maui.RevenueCat.InAppBilling, 9.0.0"
#:package Kebechet.Maui.RevenueCat.InAppBilling@9.0.0
#addin nuget:?package=Kebechet.Maui.RevenueCat.InAppBilling&version=9.0.0
#tool nuget:?package=Kebechet.Maui.RevenueCat.InAppBilling&version=9.0.0
Maui.RevenueCat.InAppBilling
A .NET MAUI wrapper library for RevenueCat in-app purchases. Provides a unified C# API that abstracts away the need for you to use platform-specific code from Android and iOS native bindings.
Features
- Unified API for iOS, Android and Mac Catalyst in-app purchases
- Subscription and one-time purchase support
- User authentication (anonymous and identified users)
- Subscription status and entitlement checking
- Trial/intro discount eligibility
- Subscriber attributes management
- Test Store
test_…keys work in Release builds on iOS - the shipped xcframework is built from source withBYPASS_SIMULATED_STORE_RELEASE_CHECK, so it does not force-close the app - Stub implementation for Windows (for development convenience)
Installation
dotnet add package Kebechet.Maui.RevenueCat.InAppBilling
Quick Start
1. Register the service
In your MauiProgram.cs:
builder.Services.AddRevenueCatBilling();
2. Inject and initialize
In App.xaml.cs, inject IRevenueCatBilling and initialize in OnStart():
public partial class App : Application
{
private readonly IRevenueCatBilling _revenueCat;
public App(IRevenueCatBilling revenueCat)
{
InitializeComponent();
_revenueCat = revenueCat;
}
protected override void OnStart()
{
var revenueCatApiKey = string.Empty;
#if __ANDROID__
revenueCatApiKey = "<your-android-api-key>";
#elif __IOS__
revenueCatApiKey = "<your-ios-api-key>";
#endif
_revenueCat.Initialize(revenueCatApiKey);
base.OnStart();
}
}
Important: Initialize must be called in
OnStart(), not in the constructor.
Tip: If you already know your user's ID at startup, use
Initialize(revenueCatApiKey, appUserId)instead - RevenueCat then never creates an anonymous user ($RCAnonymousID:...), so your custom ID won't show up as an alias of an anonymous customer.
Test Store API keys
RevenueCat's Test Store lets you exercise the full purchase flow without configuring App Store Connect / Play Console products. Test Store keys start with test_.
This wrapper ships an iOS xcframework built from source with the BYPASS_SIMULATED_STORE_RELEASE_CHECK Swift compilation flag, so a test_… key works on iOS Release builds too — it does not force-close the app. No code changes are required on your side; just pass the test_… key to Initialize(...).
On Android no such rebuild is needed: the RevenueCat SDK ships as a normal AAR (Kotlin bytecode), so its test-key check runs against the consuming app's build at runtime rather than being compiled into the library — a prebuilt AAR doesn't carry the always-on guard the prebuilt iOS framework did. Test Store support does require RevenueCat Android SDK 9.9.0+; this binding ships upstream SDK 10.16.1 (binding package 10.16.1), so test_… keys work out of the box (see issue #95, resolved by that SDK bump). The same "don't ship a test_ key to production" guidance applies on both platforms.
See src/Maui.RevenueCat.iOS/README.md for the build-time details and issue #116 for context.
API Reference
Initialization & State
| Method | Description |
|---|---|
Initialize(string apiKey) |
Initialize RevenueCat with your platform-specific API key |
Initialize(string apiKey, string appUserId) |
Initialize with a custom App User ID - no anonymous user is created, so no alias is added later |
IsInitialized() |
Check if the SDK has been initialized |
IsAnonymous() |
Check if current user is anonymous |
GetAppUserId() |
Get the current user ID |
Offerings & Products
| Method | Description |
|---|---|
GetOfferings(bool forceRefresh = false) |
Fetch available offerings and packages. Value is List<OfferingDto> |
CheckTrialOrIntroDiscountEligibility(List<string> identifiers) |
Check eligibility for trials/intro pricing. Value is Dictionary<string, IntroElegibilityStatus>. Apple platforms only (iOS and Mac Catalyst); throws NotImplementedException on Android |
Purchases
| Method | Description |
|---|---|
PurchaseProduct(PackageDto package) |
Initiate a purchase flow. Returns PurchaseResultDto - Value is the refreshed CustomerInfoDto, plus a Transaction |
GetActiveSubscriptions() |
Get list of active subscription identifiers. Value is List<string> |
GetAllPurchasedIdentifiers() |
Get all purchased product identifiers. Value is List<string> |
GetPurchaseDateForProductIdentifier(string productSku) |
Get purchase date for a specific product. Value is DateTime?; null means never purchased |
RestoreTransactions() |
Restore previous purchases. Value is CustomerInfoDto |
User Management
| Method | Description |
|---|---|
Login(string appUserId) |
Log in an identified user. Value is CustomerInfoDto |
Logout() |
Log out and create anonymous user. Value is CustomerInfoDto |
GetCustomerInfo() |
Get current customer info and entitlements. Value is CustomerInfoDto |
GetManagementSubscriptionUrl() |
Get URL for subscription management. Value is string?; null means no store-managed subscription |
Subscriber Attributes
| Method | Description |
|---|---|
SetEmail(string email) |
Set user's email |
SetDisplayName(string name) |
Set user's display name |
SetPhoneNumber(string phone) |
Set user's phone number |
SetAttributes(IDictionary<string, string> attributes) |
Set custom attributes |
Example: Complete Purchase Flow
public class PurchaseService
{
private readonly IRevenueCatBilling _revenueCat;
public PurchaseService(IRevenueCatBilling revenueCat)
{
_revenueCat = revenueCat;
}
public async Task<bool> PurchaseSubscription()
{
var offeringsResult = await _revenueCat.GetOfferings();
var offerings = offeringsResult.Value;
if (offeringsResult.IsError || offerings is null || !offerings.Any())
return false;
var defaultOffering = offerings.FirstOrDefault(o => o.IsCurrent);
var monthlyPackage = defaultOffering?.AvailablePackages
.FirstOrDefault(p => p.Identifier == "monthly");
if (monthlyPackage == null)
return false;
var result = await _revenueCat.PurchaseProduct(monthlyPackage);
if (result.IsSuccess)
{
// Purchase successful
return true;
}
if (result.Error == PurchaseErrorStatus.PurchaseCancelledError)
{
// User cancelled - not an error
return false;
}
// Handle other errors
Console.WriteLine($"Purchase failed: {result.Error}");
return false;
}
public async Task<bool> HasActiveSubscription(string entitlementId)
{
var customerInfoResult = await _revenueCat.GetCustomerInfo();
return customerInfoResult.Value?.ActiveSubscriptions
.Any(e => e == entitlementId) ?? false;
}
}
Platform Support
| Platform | Support |
|---|---|
| Android | Full implementation |
| iOS | Full implementation |
| Windows | Stub (returns defaults) |
| MacCatalyst | Full implementation (shares the iOS implementation) |
Stub implementations return a successful result (IsSuccess == true, no Error) whose Value is
an empty collection, an empty CustomerInfoDto, string.Empty for the storefront country code, or
null for GetManagementSubscriptionUrl() and GetPurchaseDateForProductIdentifier(). This allows
you to build and test on Windows without platform conditionals.
Error Handling
The library follows a non-throwing approach for runtime errors:
- Exceptions are thrown only for developer mistakes (e.g., calling methods before
Initialize()) - Runtime errors (network issues, store problems, etc.) come back as a failed result
Every asynchronous member returns a named result DTO built on
DataResult<TValue, PurchaseErrorStatus> from the
Kebechet.Types.Result package, so they are all branched on the same way:
var restoreResult = await _revenueCat.RestoreTransactions();
if (restoreResult.IsSuccess)
{
var activeSubscriptions = restoreResult.Value?.ActiveSubscriptions;
}
else
{
Console.WriteLine($"Restore failed: {restoreResult.Error} {restoreResult.ErrorException?.Message}");
}
| Member | Meaning |
|---|---|
IsSuccess / IsError |
Whether the call produced a failure |
Error |
The PurchaseErrorStatus to branch on - a closed, documented set you can switch over exhaustively |
ErrorException |
The raw store SDK exception, for logs and diagnostics. Never surface it to users |
Value |
The payload. Only meaningful when IsSuccess, and IsSuccess does not narrow it - null-check it |
The result DTOs are thin named subclasses, so signatures stay readable and each type documents its own payload:
| Result DTO | Value |
Returned by |
|---|---|---|
CanMakePaymentsResultDto |
bool |
CanMakePayments() |
IntroEligibilityResultDto |
Dictionary<string, IntroElegibilityStatus> |
CheckTrialOrIntroDiscountEligibility() |
OfferingsResultDto |
List<OfferingDto> |
GetOfferings() |
ProductIdentifiersResultDto |
List<string> |
GetActiveSubscriptions(), GetAllPurchasedIdentifiers() |
PurchaseDateResultDto |
DateTime? |
GetPurchaseDateForProductIdentifier() |
ManagementUrlResultDto |
string? |
GetManagementSubscriptionUrl() |
StorefrontResultDto |
string? |
GetStorefrontCountryCode() - empty string when the storefront is not yet known |
CustomerInfoResultDto |
CustomerInfoDto |
Login(), Logout(), RestoreTransactions(), GetCustomerInfo() |
PurchaseResultDto |
CustomerInfoDto |
PurchaseProduct() - carries the same payload as CustomerInfoResultDto plus a Transaction, but is a sibling of it, not a subclass |
ErrorException.Message is platform-specific and may include the RevenueCat backend code and
message (e.g. 7255 alias limit reached), but that is not guaranteed - branch on Error, log
ErrorException.
Only PurchaseProduct() has a user-facing cancel. On every other member,
PurchaseCancelledError means the CancellationToken you passed fired - not that the user
cancelled anything.
Common Error Codes
| Error | Description |
|---|---|
PurchaseCancelledError |
User cancelled the purchase |
StoreProblemError |
Issue with the app store |
NetworkError |
Network connectivity issue |
ProductAlreadyPurchasedError |
Product was already purchased |
PaymentPendingError |
Payment is pending (e.g., awaiting approval) |
See PurchaseErrorStatus for the complete list.
Credits
- Native bindings for Android and iOS inspired by thisisthekap's Xamarin bindings
- Abstraction layer based on RevenueCatXamarin by BillFulton
Contributing
Feel free to create an issue or pull request. For major changes, please open an issue first to discuss your proposal - large PRs without prior discussion may be rejected.
License
This project is licensed under the MIT License.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 is compatible. net9.0-android was computed. net9.0-android35.0 is compatible. net9.0-browser was computed. net9.0-ios was computed. net9.0-ios18.0 is compatible. net9.0-maccatalyst was computed. net9.0-maccatalyst18.0 is compatible. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net9.0-windows10.0.19041 is compatible. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net9.0
- Kebechet.Extensions.IsNullOrEmpty (>= 1.0.1)
- Kebechet.Types.Result (>= 1.0.0 && < 2.0.0)
- Microsoft.Maui.Controls (>= 9.0.0)
-
net9.0-android35.0
- Kebechet.Extensions.IsNullOrEmpty (>= 1.0.1)
- Kebechet.Maui.RevenueCat.Android (>= 10.16.1)
- Kebechet.Types.Result (>= 1.0.0 && < 2.0.0)
- Microsoft.Maui.Controls (>= 9.0.0)
-
net9.0-ios18.0
- Kebechet.Extensions.IsNullOrEmpty (>= 1.0.1)
- Kebechet.Maui.RevenueCat.iOS (>= 5.83.1)
- Kebechet.Types.Result (>= 1.0.0 && < 2.0.0)
- Microsoft.Maui.Controls (>= 9.0.0)
-
net9.0-maccatalyst18.0
- Kebechet.Extensions.IsNullOrEmpty (>= 1.0.1)
- Kebechet.Maui.RevenueCat.iOS (>= 5.83.1)
- Kebechet.Types.Result (>= 1.0.0 && < 2.0.0)
- Microsoft.Maui.Controls (>= 9.0.0)
-
net9.0-windows10.0.19041
- Kebechet.Extensions.IsNullOrEmpty (>= 1.0.1)
- Kebechet.Types.Result (>= 1.0.0 && < 2.0.0)
- Microsoft.Maui.Controls (>= 9.0.0)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on Kebechet.Maui.RevenueCat.InAppBilling:
| Package | Downloads |
|---|---|
|
benxu.AppPlatform.Billing.RevenueCat
RevenueCat integration for in-app subscriptions in .NET MAUI. Provides real SDK integration for Android/iOS with optional simulator mode for testing. |
|
|
SyntaxCircus.RevenueCat.Maui
Client-side RevenueCat helpers for MAUI apps: SDK initialization, identity sync on login, offering-to-DTO mapping, and a purchase/restore orchestrator built on Kebechet.Maui.RevenueCat.InAppBilling. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 9.0.0 | 195 | 9/1/2026 |
| 8.0.0 | 342 | 8/15/2026 |
| 7.4.0 | 725 | 8/7/2026 |
| 7.3.2 | 164 | 8/5/2026 |
| 7.3.1 | 140 | 8/5/2026 |
| 7.3.0 | 125 | 8/5/2026 |
| 7.2.0 | 116 | 8/5/2026 |
| 7.1.0 | 1,380 | 6/17/2026 |
| 7.0.1 | 859 | 6/10/2026 |
| 7.0.0 | 514 | 5/19/2026 |
| 6.0.0 | 417 | 5/13/2026 |
| 5.5.0 | 1,136 | 2/8/2026 |
| 5.4.4 | 1,172 | 1/10/2026 |
| 5.4.3 | 213 | 1/4/2026 |
| 5.4.2 | 729 | 12/7/2025 |
| 5.4.1 | 549 | 11/3/2025 |
| 5.4.0 | 252 | 11/2/2025 |
| 5.3.2 | 631 | 10/11/2025 |
| 5.3.1 | 217 | 10/11/2025 |
| 5.3.0 | 762 | 8/21/2025 |
BREAKING
- Every asynchronous `IRevenueCatBilling` member now returns a result DTO instead of a bare value. Previously a network blip, a cancellation and a backend rejection were indistinguishable from a legitimately empty result - the RevenueCat error only ever reached the log. Read `.Value` on success; branch on `.Error`; log `.ErrorException`.
- `CanMakePaymentsResultDto`, `IntroEligibilityResultDto`, `OfferingsResultDto`, `ProductIdentifiersResultDto`, `PurchaseDateResultDto`, `ManagementUrlResultDto`, `StorefrontResultDto`, `CustomerInfoResultDto` and `PurchaseResultDto`. Each is a thin named subclass of `DataResult<TValue, PurchaseErrorStatus>` from the `Kebechet.Types.Result` package, so signatures stay readable and every result shares one contract.
- `PurchaseProduct()` keeps returning `PurchaseResultDto`, now a `DataResult<CustomerInfoDto, PurchaseErrorStatus>` subclass: `ErrorStatus` becomes `Error`, `ErrorMessage` becomes `ErrorException`, `CustomerInfo` becomes `Value`, and `Transaction` stays. It is a sibling of `CustomerInfoResultDto`, not a subclass of it.
- `PurchaseResultDto` is a `class`, not a `sealed record`. Value equality, `with` expressions and the record `ToString()` are gone, so `resultA == resultB` is now reference equality rather than a compile error or a value comparison. The other result DTOs are classes for the same reason - they derive from the `Kebechet.Types.Result` base types.
- `CustomerInfoDto.ManagementURL` is renamed to `CustomerInfoDto.ManagementUrl`, matching .NET acronym casing.
- New public dependency on `Kebechet.Types.Result` (1.0.0). It cannot be hidden behind `PrivateAssets` because the types appear in the public signatures.
ADDED
- `PurchaseErrorStatus` is now reported by every fallible member, so the documented failure modes can be handled exhaustively with a `switch`.
- `GetPurchaseDateForProductIdentifier()` and `GetManagementSubscriptionUrl()` distinguish "not purchased" / "no store-managed subscription" (success with a null `Value`) from a failure (`Error` set).
FIXED
- Android `GetManagementSubscriptionUrl()` returned `null` both when the user had no store subscription and when the call failed; iOS returned `string.Empty` for the former. The two cases are now distinguishable, and the platforms agree.
- `PurchaseProduct()` returned a result that was neither `IsSuccess` nor `IsError` when the transaction did not reach the purchased state. That state now reports an error while still carrying the `Transaction`: `PaymentPendingError` when the store says the payment is still settling (iOS `Deferred`, Android `PENDING`), and `UnknownError` for every other non-purchased state, which is an anomaly rather than something the user can wait out.
- `PurchaseProduct()` logged nothing at all when the native error code mapped to a cancellation - it skipped the error log without logging anything else. Every member now logs cancellation at debug and other failures at error, through one shared classifier.
- Removed the `_cachedManagementUrl` field. It was read on both platforms but never assigned, so its early-return guard could never fire. Caching it would have been wrong regardless: the URL is per-customer, so a process-lifetime cache would survive `Login`/`Logout`.
- The Windows and PlatformsStandard stubs no longer report `IsSuccess == false` together with `IsError == false` for `PurchaseProduct()`, a state that satisfied neither branch of the documented pattern. Every stub member now reports an empty success, and `GetStorefrontCountryCode()` keeps the documented `string.Empty` sentinel rather than `null`.
NOTE
- Exceptions are still thrown for developer mistakes, such as calling a member before `Initialize()`, and `CheckTrialOrIntroDiscountEligibility()` still throws `NotImplementedException` on Android.