.NET MAUI Setup

maui-iap is the .NET MAUI projection of OpenIAP. Install the single NuGet package, connect to the store, fetch products, listen for purchase events, then finish transactions after your server has verified them.

Prerequisites#

RequirementDetails
.NET.NET 10 SDK and the MAUI workload: dotnet workload install maui
iOS / macCatalystiOS 15+ / macCatalyst 15+, Apple Developer account, matching bundle identifier, In-App Purchase capability, sandbox tester.
AndroidAndroid API 24+, Google Play Developer account, matching package name, license tester, uploaded build on a test track
DeviceUse a physical device for real purchase testing. Simulators and emulators are useful for UI checks, but store purchase support is limited.

Installation#

Add the package to your MAUI app project:

sh
dotnet add package OpenIap.Maui

This resolves the latest stable package from NuGet. If your project pins package versions manually, use the current NuGet package reference:

xml
<ItemGroup>
  <PackageReference Include="OpenIap.Maui" Version="2.5.0" />
</ItemGroup>

Your app references a single package, OpenIap.Maui. The OpenIAP-owned iOS and Android bindings are bundled inside it, while shared dependencies (Google Play Billing, Play Services, AndroidX, Kotlin, Gson) remain ordinary NuGet dependencies so NuGet can deduplicate them with the rest of your dependency graph.

If you are working from this monorepo before publishing, use a project reference to the main project only. The example app re-declares local native references because MSBuild does not propagate those transitively through ProjectReference. Published NuGet consumers do not need that.

xml
<ProjectReference Include="path/to/openiap/libraries/maui-iap/src/OpenIap.Maui/OpenIap.Maui.csproj" />

Building the Apple library from source requires Xcode 27; the published package already embeds a prebuilt XCFramework, so NuGet consumers do not need Xcode 27 — their normal toolchain is enough.

Project Configuration#

Configure the app project once per platform before calling any store API.

Target Frameworks#

Include the platforms you ship in your MAUI app's TargetFrameworks:

xml
<TargetFrameworks>net10.0-ios;net10.0-android;net10.0-maccatalyst</TargetFrameworks>

<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">15.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">15.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">24.0</SupportedOSPlatformVersion>

iOS / macCatalyst#

  • Set the app's bundle identifier to the same value you configured in App Store Connect.
  • Enable In-App Purchase in Signing & Capabilities for the app identifier and provisioning profile.
  • Sign into the device with a sandbox tester when testing App Store purchases.

Android#

  • Set the Android package name to the same package configured in Play Console.
  • Add the Play Billing permission to Platforms/Android/AndroidManifest.xml.
  • Upload a build with the same package name and signing lineage to an internal or closed test track before testing purchases.
xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
  <uses-permission android:name="com.android.vending.BILLING" />
</manifest>

Usage#

The typical flow is: initialize the connection with InitConnectionAsync, fetch products with FetchProductsAsync, register the PurchaseUpdated and PurchaseError listeners, request a purchase with RequestPurchaseAsync, then finish the verified transaction with FinishTransactionAsync. The Purchase Guide covers the complete flow.

Initialize and Fetch Products#

Use OpenIapClient.Instance as the entry point. Cast it to the generated resolver interfaces when calling query or mutation APIs.

cs
using OpenIap;
using OpenIap.Maui;

var iap = OpenIapClient.Instance;
var query = (QueryResolver)iap;
var mutate = (MutationResolver)iap;

var connected = await mutate.InitConnectionAsync();
if (!connected) throw new InvalidOperationException("Store connection failed");

var result = await query.FetchProductsAsync(new ProductRequest
{
    Skus = new[] { "premium", "coins_100" },
    Type = ProductQueryType.InApp,
});

var products = result is FetchProductsResultProducts { Value: { } list }
    ? list
    : Array.Empty<Product>();

Each call here has a full reference — see InitConnectionAsync and FetchProductsAsync for parameters and per-store behavior.

Listen Before Requesting a Purchase#

Purchase APIs are event-based. Register listeners before calling RequestPurchaseAsync, then finish the transaction only after server-side verification succeeds. See PurchaseUpdated, PurchaseError, and FinishTransactionAsync for parameters and per-store behavior, and Error Codes for the error.Code values.

cs
async Task HandlePurchaseSafelyAsync(Purchase purchase)
{
    try
    {
        bool verified = await VerifyOnServerAsync(purchase);
        if (!verified) return;

        await mutate.FinishTransactionAsync(
            purchase: new PurchaseInput(purchase),
            isConsumable: true);

        GrantEntitlement(purchase.ProductId);
    }
    catch (Exception error)
    {
        Console.WriteLine(
quot;Purchase processing failed: {error.Message}"); } } IDisposable purchaseSub = iap.PurchaseUpdated.Subscribe( purchase => _ = HandlePurchaseSafelyAsync(purchase)); IDisposable errorSub = iap.PurchaseError.Subscribe(error => { Console.WriteLine(
quot;{error.Code}: {error.Message}"); }); await mutate.RequestPurchaseAsync(new RequestPurchaseProps { Type = ProductQueryType.InApp, RequestPurchase = new RequestPurchasePropsByPlatforms { Apple = new RequestPurchaseIosProps { Sku = "coins_100", Quantity = 1, }, Google = new RequestPurchaseAndroidProps { Skus = new[] { "coins_100" }, }, }, });

IAPKit API#

IAPKit is OpenIAP's hosted purchase-validation and entitlement backend (see Purchase Verification with IAPKit). The MAUI package ships the same app-facing helper as the other OpenIAP SDKs: create a kit client with your publishable key to read purchase status and entitlements and to bind a purchase to a user. Store lifecycle events (App Store Server Notifications, Google Play RTDN) are delivered to IAPKit's backend, not to your app — the client reads current state through these bounded calls rather than subscribing to a webhook stream.

cs
using OpenIap;
using OpenIap.Maui;

var kit = OpenIapClient.KitApi(new KitApiOptions
{
    ApiKey = "openiap-kit_pk_<your-publishable-key>",
    BaseUrl = "https://kit.openiap.dev",
});

StatusResponse status = await kit.StatusAsync("user_123");
EntitlementsResponse entitlements = await kit.EntitlementsAsync("user_123");
BindUserResponse bind = await kit.BindUserAsync(purchase.PurchaseToken!, "user_123");

Cleanup#

Dispose subscriptions and close the store connection when the page or service that owns the purchase flow is torn down. See EndConnectionAsync for its full reference.

cs
purchaseSub.Dispose();
errorSub.Dispose();

var ended = await mutate.EndConnectionAsync();
if (!ended) Console.WriteLine("Store teardown did not complete");

Example App#

The reference app lives at libraries/maui-iap/example/OpenIap.Maui.Example. It mirrors the Expo example screens: Home, All Products, In-App Purchase Flow, Subscription Flow, Available Purchases, Offer Code, Alternative Billing.

The example app builds against the in-repo Android library, so rebuild the OpenIAP Android AARs once before the first run (published-package consumers skip this step):

sh
# From the OpenIAP repo root:
(cd packages/google && ./gradlew :openiap:assemblePlayRelease)
(cd libraries/maui-iap/android && ../../../packages/google/gradlew :openiap:assembleRelease)

Then run the example (its application id is dev.hyo.martie; uninstalling first clears stale native code):

sh
cd libraries/maui-iap/example/OpenIap.Maui.Example

# Android device or emulator
adb uninstall dev.hyo.martie || true
dotnet build -t:Run -f net10.0-android

# iOS device or simulator
dotnet build -t:Run -f net10.0-ios

# macCatalyst
dotnet build -t:Run -f net10.0-maccatalyst

VS Code launch configurations are available in libraries/maui-iap/.vscode/launch.json. The iOS device launcher auto-selects a connected USB device when one is available, and the Android launcher builds both Android AARs before uninstalling and rebuilding the example app so stale APKs do not keep old BillingClient code.

Troubleshooting#

Common failures and their causes, roughly in the order you hit them.

Products Do Not Load#

  • Confirm the store product IDs exactly match the IDs passed in ProductRequest.Skus.
  • Confirm the bundle identifier or Android package name matches the store app record.
  • On Android, install a build from a Play test track or a locally signed build that matches the uploaded app and tester account.
  • On iOS, test on a device signed with a profile that includes the In-App Purchase capability.

Android Billing Not Configured#

If Google Play shows "This version of the application is not configured for billing through Google Play", the library reached BillingClient correctly - Play itself rejected the app build. Check each of the following:

  • The installed build's package name and signing key match the uploaded build.
  • The build is on an internal or closed test track.
  • The signed-in account is a license tester.
  • The products are active in Play Console.

Android Old BillingClient Error#

If Android reports a missing enableAutoServiceReconnection method, uninstall the stale APK and rebuild with the current package. That error means an older BillingClient was still present in the installed app.

sh
adb uninstall dev.hyo.martie || true
dotnet clean
dotnet build -t:Run -f net10.0-android

Android Build File Lock#

XABLD7024 usually means a previous MAUI build or deploy process still holds a generated file under obj/. Stop the running app, close duplicate dotnet build processes, then clean the example project before rebuilding.

iOS Stays on Connecting#

Verify the app is running on a signed device build with the matching bundle identifier and In-App Purchase capability. If you navigate away from a purchase screen, call EndConnectionAsync from the owning page or lifecycle service so the next screen can initialize a fresh store connection.

Next Steps#