In-app nákupy v .NET MAUI 2026: Google Play Billing 8 a StoreKit 2 po zániku Plugin.InAppBilling

Prechod z archivovaného Plugin.InAppBilling na natívne API. Google Play Billing 8, StoreKit 2 a ako správne validovať príjmy na .NET backende. Praktické ukážky z produkčných MAUI 10 projektov.

In-app nákupy v .NET MAUI 2026: Sprievodca

Aktualizované: 25. júla 2026

In-app nákupy v .NET MAUI sa v roku 2026 robia cez platformovo-natívne API (Google Play Billing Library 8 na Androide a StoreKit 2 na iOS/Mac Catalyst), pretože komunitný Plugin.InAppBilling je archivovaný a Microsoft odporúča vlastnú tenkú abstrakciu nad natívnymi službami podľa jeho referenčnej BillingService ukážky. Poviem to rovno: v tomto článku ukážem, ako túto abstrakciu postaviť v produkčnom .NET MAUI 10 projekte, ako správne validovať príjmy na serveri a ako sa vyhnúť pasciam, ktoré nás v tímoch pri prechode z Xamarin.Formsu stáli týždne. Sám som na tri z nich narazil pri IAP migrácii pre EdTech klienta v januári 2026, tak sa ich pokúsim opísať tak, aby vás nezastihli.

  • Plugin.InAppBilling je od apríla 2025 archivovaný. Nemá update pre Google Play Billing 8 a nezachytáva StoreKit 2 async API, takže sa v novo publikovaných apkách používať naďalej nedá.
  • Google Play Billing Library 8 je povinná do 31. augusta 2026 pre všetky nové aplikácie aj aktualizácie; predĺženie je možné maximálne do 1. novembra 2026.
  • StoreKit 2 nahradil verifyReceipt. Na serverovú validáciu sa dnes používa App Store Server API s JWT autorizáciou a originalTransactionId.
  • Microsoftov BillingService sample je najbližšie k oficiálnemu odporúčaniu: tenká C# abstrakcia s platform-specific implementáciami cez DI.
  • Serverová validácia nie je voliteľná. Bez nej sa dá klient obísť za desiatky minút; opísanie zapojenia s vlastným backendom nájdete nižšie.
  • RevenueCat a IAPHUB sú racionálna voľba pre tímy do troch vývojárov, ktorí nechcú udržiavať vlastný validačný server.

Prečo je monetizácia v .NET MAUI v roku 2026 iná

V roku 2024 sme si v tíme mohli dovoliť odkázať Plugin.InAppBilling ako nuget, implementovať tri metódy a mať IAP „hotové". V roku 2026 je situácia iná z troch dôvodov, ktoré sa udiali paralelne. Každý z nich sám osebe by stačil ako dôvod na prepísanie IAP vrstvy.

Po prvé, James Montemagno v apríli 2025 archivoval repozitár Plugin.InAppBilling. V README priamo napísal, že IAP API sú príliš komplexné, StoreKit 1 je deprecated a Android Billing Library sa mení tak rýchlo, že jeden človek to vo voľnom čase nezvláda. Nikto plugin oficiálne neforkol. Existujú síce súkromné forky, no žiaden nemá kritický počet používateľov, aby sme mu v produkcii dôverovali.

Po druhé, Google Play Billing Library 8 vyšla 30. júna 2025 a jej používanie sa stalo povinným pre všetky nové publikované aplikácie a aktualizácie do 31. augusta 2026. Naša aplikácia môže mať PBL 7 zabudovanú a stále bude na Google Play fungovať pre existujúcich používateľov, ale žiadny update nepustíme cez Play Console po tomto dátume, pokiaľ nebudeme na 8. Overiť si to viete v oficiálnej Google FAQ o deprecation cykle.

Po tretie, Apple v roku 2025 prakticky uzavrel starý verifyReceipt endpoint pre novú validáciu a všetky nové projekty tlačí smerom k App Store Server API s JWT. Kto má vlastný backend v .NET, musí prerobiť validačnú logiku, pretože potvrdenie príjmu už nie je opaque base64 blob, ale podpísaný JWS.

Kombinácia týchto troch faktov znamená jedno: aj tímy, ktoré mali IAP „vyriešené" v Xamarin.Forms alebo v ranom MAUI, musia v roku 2026 IAP vrstvu prepísať. A pretože oficiálna Microsoft cesta znie „napíšte si vlastnú tenkú abstrakciu podľa nášho sample-u", tak je to práve tá tenká abstrakcia, o ktorú tu ide.

Čo nahradilo Plugin.InAppBilling po jeho archivácii

Odpoveď je nepríjemná: nič priamo. Neexistuje ekvivalentný komunitný plugin, ktorý by sme jednou using direktívou mohli nasadiť ako drop-in replacement. Namiesto toho sú v hre tri praktické cesty a každá má iné náklady.

1. Microsoft BillingService sample (odporúčaná pre stredné a väčšie tímy)

Microsoft v roku 2025 zverejnil BillingService ukážkový projekt, ktorý ukazuje presne to, čo teraz robí každý serióznejší MAUI tím: partial class s tromi implementáciami (Android, iOS/Mac, Windows), registrovaná cez DI, zaobalená v ViewModeli. Nie je to nuget, ktorý si stiahneme. Je to kód, ktorý si skopírujeme a upravíme. Presne to je zámer.

2. Third-party komerčné SDK (RevenueCat, IAPHUB, Adapty)

Tieto firmy predávajú zjednotenú C# API s vlastnou backend infraštruktúrou pre validáciu, analytiku a A/B testovanie predplatných. IAPHUB má oficiálny .NET MAUI SDK, RevenueCat od začiatku roku 2026 tiež oficiálne podporuje MAUI cez binding balíček. Cena (typicky 1 % z tržieb nad určitú hranicu) je pre tímy do troch ľudí spravidla lacnejšia ako mesiace vlastnej práce.

3. Priama práca s natívnymi API cez binding library

Pre Android sa dá cez Xamarin.Android.Google.BillingClient nuget balíček dostať k BillingClient triede z Google Play Billing 8. Balíček historicky mal problémy s namespaceom v čistom .NET MAUI (viď issue #30402 v repozitári dotnet/maui), ale v .NET 10 sú tieto problémy prevažne vyriešené. Pre iOS je situácia jednoduchšia: StoreKit 2 API sú v Microsoft.iOS vystavené prakticky 1:1 a čakáme len na plný Swift interop pre pár novších WWDC 25 doplnení.

V našich tímoch preferujeme cestu 1 pre projekty, ktoré máme finančne pod kontrolou (tam, kde 1 % z tržieb je zbytočne veľa), a cestu 2 pre projekty, ktoré rýchlo škálujú a nemôžeme si dovoliť backendový tím na validáciu. Cestu 3 volíme len keď potrebujeme prístup ku špecifickému native features, ktoré abstrakcie ešte nepokrývajú (napríklad Play Points integrácia).

Google Play Billing Library 8: termíny a čo znamenajú pre .NET MAUI

Google má na PBL dvojročný deprecation cyklus. Každá major verzia získa GA date, potom má rok „soft" a rok „hard" deadline. Verzia 8 vyšla 30. júna 2025 a jej hard deadline je 31. augusta 2026. Kľúčové v tomto termíne je slovo „publish": nejde o runtime block, ide o publikačný block. Aplikácie s PBL 7, ktoré už sú v obchode, budú ďalej vykonávať transakcie. Ale novú verziu do Play Console nenahráme.

AspektGoogle Play Billing 7Google Play Billing 8
GA dátum202430. jún 2025
Publish deadline31. august 2025 (v minulosti)31. august 2026
Extension windowN/ADo 1. novembra 2026
queryPurchaseHistoryAsyncPrítomnéOdstránené (nebolo autoritatívne)
Nové API pre one-time productsNieÁno
Suspended subscriptionsNieÁno (v8.1+)
.NET MAUI binding podporaProblematickáStabilná v .NET 10

Praktické dôsledky pre náš kód: metódu QueryPurchaseHistoryAsync už nebudeme vôbec volať, pretože bola len „nice-to-have" a klient nikdy nemal byť autoritou o histórii nákupov (autoritou je Play backend, prípadne náš vlastný backend). Namiesto toho voláme QueryPurchasesAsync, ktorá vráti len aktívne, nespotrebované, nepotvrdené transakcie, a to je presne to, čo pri štarte aplikácie potrebujeme, aby sme obnovili prístup k platenému obsahu.

Ďalej: PBL 8 vyžaduje explicitné acknowledgePurchase volanie do 3 dní od nákupu, inak Google transakciu automaticky vráti. V predchádzajúcich verziách bol acknowledge tiež povinný, ale mnohí vývojári to obchádzali cez okamžité consume pri consumables. V 8-ke sú pravidlá presnejšie a Google ich vynucuje aktívne.

StoreKit 2 v .NET MAUI: async model a App Store Server API

StoreKit 2 (dostupný od iOS 15, plne funkčný od iOS 17+) je Apple prepis pôvodného StoreKit 1 do moderného async/await modelu. Pre nás v .NET MAUI to znamená dve praktické veci: transakcie sa asynchronne streamujú cez Transaction.Updates a samotný JWS podpis nákupu vieme validovať kryptograficky priamo na zariadení. Apple publikuje verejný kľúč, ktorý zabalený v OS umožňuje overiť, že transakčné dáta pochádzajú z App Store a neboli zmenené.

V .NET MAUI 10 sú StoreKit 2 typy vystavené v namespace StoreKit cez štandardný Microsoft.iOS binding. Voláme priamo SKProduct.RequestProductsAsync, Product.PurchaseAsync a listenujeme na Transaction.Updates stream, ktorý vytvorí async enumerable. Kód vyzerá takmer identicky ako Swift verzia z Apple dokumentácie.

Kľúčový rozdiel oproti StoreKit 1: už nemáme jeden monolitický „receipt" na zariadení. Namiesto toho každá transakcia má vlastnú JWSRepresentation, čo je podpísaný token, ktorý obsahuje product ID, transaction ID, timestamp a bundle ID. Tento token pošleme na náš server. Server buď kryptograficky overí podpis lokálne (rýchle), alebo, čo je odporúčané pre subscription status, zavolá App Store Server API s JWT autorizáciou a získa čerstvý stav predplatného.

Implementácia cross-platform IAP servisu v .NET MAUI

Nasledujúca implementácia vychádza z BillingService sample-u, ktorý sme v produkčnej aplikácii pre klienta upravili do štruktúry, akú používame naprieč projektami. Základná myšlienka: jedno rozhranie IBillingService, tri partial implementácie (Android, iOS/Mac Catalyst, Windows), zaregistrované cez DI a konzumované z ViewModelu. Rovnaký prístup, aký odporúčame v našom sprievodcovi MVVM architektúrou s CommunityToolkitom a DI.

Zdieľané rozhranie

// Services/IBillingService.cs
public interface IBillingService
{
    Task<IReadOnlyList<Product>> GetProductsAsync(IEnumerable<string> productIds);
    Task<PurchaseResult> PurchaseAsync(string productId);
    Task<IReadOnlyList<Purchase>> RestorePurchasesAsync();
    Task<bool> AcknowledgeAsync(string purchaseToken);
}

public record Product(string Id, string Title, string Description, string LocalizedPrice, ProductType Type);
public record Purchase(string ProductId, string TransactionId, string SignedPayload, DateTimeOffset PurchasedAt);
public record PurchaseResult(bool Success, Purchase? Purchase, string? ErrorMessage);

public enum ProductType { Consumable, NonConsumable, Subscription }

Registrácia v MauiProgram.cs

// MauiProgram.cs
builder.Services.AddSingleton<IBillingService, BillingService>();
builder.Services.AddSingleton<IReceiptValidator, ServerReceiptValidator>();

Android implementácia (Google Play Billing 8)

// Services/BillingService.Android.cs
using Android.BillingClient.Api;

public partial class BillingService : IBillingService, IPurchasesUpdatedListener, IBillingClientStateListener
{
    private BillingClient? _client;
    private TaskCompletionSource<bool>? _connectionTcs;
    private TaskCompletionSource<PurchaseResult>? _purchaseTcs;

    private async Task EnsureConnectedAsync()
    {
        if (_client?.IsReady == true) return;

        _client = BillingClient.NewBuilder(Platform.CurrentActivity!)
            .SetListener(this)
            .EnablePendingPurchases(PendingPurchasesParams.NewBuilder()
                .EnableOneTimeProducts()
                .Build())
            .Build();

        _connectionTcs = new TaskCompletionSource<bool>();
        _client.StartConnection(this);
        await _connectionTcs.Task;
    }

    public async Task<IReadOnlyList<Product>> GetProductsAsync(IEnumerable<string> productIds)
    {
        await EnsureConnectedAsync();

        var productList = productIds.Select(id =>
            QueryProductDetailsParams.Product.NewBuilder()
                .SetProductId(id)
                .SetProductType(BillingClient.ProductType.Inapp)
                .Build()).ToList();

        var queryParams = QueryProductDetailsParams.NewBuilder()
            .SetProductList(productList)
            .Build();

        var result = await _client!.QueryProductDetailsAsync(queryParams);
        return result.ProductDetailsList.Select(MapProduct).ToList();
    }

    public async Task<PurchaseResult> PurchaseAsync(string productId)
    {
        await EnsureConnectedAsync();

        var products = await GetProductsAsync(new[] { productId });
        var details = _lastQueriedDetails![productId];

        var flowParams = BillingFlowParams.NewBuilder()
            .SetProductDetailsParamsList(new[]
            {
                BillingFlowParams.ProductDetailsParams.NewBuilder()
                    .SetProductDetails(details)
                    .Build()
            })
            .Build();

        _purchaseTcs = new TaskCompletionSource<PurchaseResult>();
        _client!.LaunchBillingFlow(Platform.CurrentActivity!, flowParams);
        return await _purchaseTcs.Task;
    }

    // Callback z Google Play po dokonceni flow
    public void OnPurchasesUpdated(BillingResult result, IList<Android.BillingClient.Api.Purchase>? purchases)
    {
        if (result.ResponseCode != BillingResponseCode.Ok || purchases is null)
        {
            _purchaseTcs?.TrySetResult(new PurchaseResult(false, null, result.DebugMessage));
            return;
        }

        var p = purchases[0];
        var purchase = new Purchase(
            ProductId: p.Products[0],
            TransactionId: p.OrderId ?? p.PurchaseToken,
            SignedPayload: p.OriginalJson, // + p.Signature - posleme na server
            PurchasedAt: DateTimeOffset.FromUnixTimeMilliseconds(p.PurchaseTime));

        _purchaseTcs?.TrySetResult(new PurchaseResult(true, purchase, null));
    }

    public async Task<bool> AcknowledgeAsync(string purchaseToken)
    {
        var ackParams = AcknowledgePurchaseParams.NewBuilder()
            .SetPurchaseToken(purchaseToken)
            .Build();
        var result = await _client!.AcknowledgePurchaseAsync(ackParams);
        return result.ResponseCode == BillingResponseCode.Ok;
    }
}

iOS/Mac Catalyst implementácia (StoreKit 2)

// Services/BillingService.MaciOS.cs
using StoreKit;

public partial class BillingService : IBillingService
{
    private Task? _transactionListenerTask;

    public BillingService()
    {
        _transactionListenerTask = ListenForTransactionUpdatesAsync();
    }

    public async Task<IReadOnlyList<Product>> GetProductsAsync(IEnumerable<string> productIds)
    {
        var products = await Product.ProductsAsync(productIds.ToArray());
        return products.Select(p => new Product(
            Id: p.Id,
            Title: p.DisplayName,
            Description: p.Description,
            LocalizedPrice: p.DisplayPrice,
            Type: MapType(p.Type))).ToList();
    }

    public async Task<PurchaseResult> PurchaseAsync(string productId)
    {
        var products = await Product.ProductsAsync(new[] { productId });
        if (products.Length == 0)
            return new PurchaseResult(false, null, "Product not found");

        var result = await products[0].PurchaseAsync();

        return result switch
        {
            { Status: PurchaseResultStatus.Success } s => await HandleVerificationAsync(s.Verification),
            { Status: PurchaseResultStatus.UserCancelled } => new PurchaseResult(false, null, "Cancelled"),
            { Status: PurchaseResultStatus.Pending } => new PurchaseResult(false, null, "Pending (Ask to Buy)"),
            _ => new PurchaseResult(false, null, "Unknown")
        };
    }

    private async Task<PurchaseResult> HandleVerificationAsync(VerificationResult verification)
    {
        if (verification is VerificationResult.Unverified)
            return new PurchaseResult(false, null, "JWS verification failed");

        var transaction = ((VerificationResult.Verified)verification).Transaction;

        // signedPayload = JWS token, server ho overi cez App Store Server API
        var purchase = new Purchase(
            ProductId: transaction.ProductID,
            TransactionId: transaction.Id.ToString(),
            SignedPayload: verification.JwsRepresentation,
            PurchasedAt: transaction.PurchaseDate);

        await transaction.FinishAsync();
        return new PurchaseResult(true, purchase, null);
    }

    private async Task ListenForTransactionUpdatesAsync()
    {
        await foreach (var verification in Transaction.Updates)
        {
            if (verification is VerificationResult.Verified verified)
            {
                // Posleme na nas server na aktualizaciu stavu predplatneho
                await NotifyServerAsync(verified.Transaction);
                await verified.Transaction.FinishAsync();
            }
        }
    }
}

Tento pattern (jedno rozhranie, tri implementácie zdieľajúce ten istý DTO) vyriešil v jednom projekte pre EdTech klienta zaujímavý problém: produktový manažment ani netušil, ktorá platforma má aké špecifiká. Business kód (ViewModel) rozdiel medzi Google Play a App Store vôbec nevidí; vidí len IBillingService a jeho asynchrónne metódy. Úprimne, ušetrilo nám to niekoľko sprintov diskusií o „prečo je iOS iný ako Android".

Serverová validácia príjmov: prečo klient nikdy nestačí

Toto je časť, ktorú v každom druhom „ako urobiť IAP" tutoriále vidíme preskočenú alebo odbytú vetou „v produkcii to samozrejme validujte na serveri". V realite tímy, ktoré serverovú validáciu preskočia, do dvoch mesiacov od vydania vidia v analytike, že 8 až 15 % používateľov má aktívnu subscription bez toho, aby ju kedykoľvek zaplatili. Existujú hotové nástroje, ktoré do MAUI aplikácie injektnú falošný response z billing servisu, a klient bez serverovej validácie tomu uverí.

Serverová validácia má dve úrovne. Prvá je kryptografická: overíme, že JWS podpis (iOS) alebo Purchase.getSignature() (Android) je platný a pochádza od Apple/Google. To sa dá spraviť lokálne, bez volania Apple/Google API. Stačí verejný kľúč. Druhá je stavová: overíme aktuálny stav (aktívne? refundované? v grace period?) volaním App Store Server API alebo Google Play Developer API. To vyžaduje autentifikovaný request z nášho backendu.

Endpoint na .NET backendu

// BillingController.cs (ASP.NET Core)
[ApiController]
[Route("api/billing")]
[Authorize] // JWT z nasej aplikacie, viz auth clanok
public class BillingController : ControllerBase
{
    private readonly IAppleReceiptValidator _apple;
    private readonly IGoogleReceiptValidator _google;
    private readonly ISubscriptionRepository _repo;

    [HttpPost("validate")]
    public async Task<IActionResult> Validate([FromBody] ValidatePurchaseRequest req)
    {
        var userId = User.FindFirst(ClaimTypes.NameIdentifier)!.Value;

        var validation = req.Platform switch
        {
            "ios" => await _apple.ValidateAsync(req.SignedPayload),
            "android" => await _google.ValidateAsync(req.ProductId, req.PurchaseToken),
            _ => ValidationResult.Invalid("unknown platform")
        };

        if (!validation.IsValid)
            return BadRequest(new { error = validation.Reason });

        await _repo.UpsertEntitlementAsync(userId, validation.ProductId, validation.ExpiresAt);
        return Ok(new { entitled = true, expiresAt = validation.ExpiresAt });
    }
}

Klientský .NET MAUI kód po úspešnom nákupe zavolá tento endpoint pomocou HttpClient a nášho zdieľaného pattern-u. Viac o štruktúre servisnej vrstvy a odolnosti voči chybám nájdete v článku o REST API v .NET MAUI a HttpClient patternoch. Serverový endpoint samozrejme musí byť chránený autentifikáciou. Používame rovnaký JWT bearer setup, ktorý sme popísali v článku o OAuth2 a JWT autentifikácii v .NET MAUI.

Apple validácia cez App Store Server API

public class AppleReceiptValidator : IAppleReceiptValidator
{
    private readonly HttpClient _http;
    private readonly AppleJwtProvider _jwt; // generuje ES256 JWT z p8 kluca

    public async Task<ValidationResult> ValidateAsync(string jwsPayload)
    {
        // JWS ma 3 casti oddelene bodkou: header.payload.signature
        var parts = jwsPayload.Split('.');
        var payload = JsonSerializer.Deserialize<JwsTransactionPayload>(
            Base64UrlDecode(parts[1]));

        // Zavolame App Store Server API na overenie a cerstvy stav
        var token = _jwt.CreateToken();
        _http.DefaultRequestHeaders.Authorization = new("Bearer", token);

        var url = $"https://api.storekit.itunes.apple.com/inApps/v1/transactions/{payload.TransactionId}";
        var resp = await _http.GetFromJsonAsync<AppleTransactionResponse>(url);

        if (resp is null || resp.ExpiresDate < DateTimeOffset.UtcNow)
            return ValidationResult.Invalid("expired or missing");

        return ValidationResult.Valid(payload.ProductId, resp.ExpiresDate);
    }
}

Ako testovať in-app nákupy pred vydaním

Testovanie IAP je disciplína sama o sebe a je to miesto, kde tímy strácajú najviac času, keď robia veci prvýkrát. Nemôžeme nákupy testovať s reálnymi kartami (Apple ani Google to nedovolia pre neverejné buildy), takže obe platformy majú vlastné sandbox mechanizmy s vlastnými pravidlami.

Android: license testers v Play Console

V Google Play Console v sekcii Setup → License testing pridáme email účet ako testera. Tento účet potom pri nákupe cez tester APK (interná testovacia dráha) neplatí reálne peniaze, ale flow prebehne inak identicky. Kľúčové: APK musí byť podpísaný release kľúčom (nie debug), inak Play Store transakciu odmietne s neintuitívnou chybou o „nesprávnej konfigurácii". O vydávaní podpísaných build-ov píšeme detailne v článku o CI/CD pre .NET MAUI cez GitHub Actions.

iOS: Sandbox Apple ID a Transaction Manager

Sandbox tester si vytvoríme v App Store Connect (Users and Access → Sandbox Testers). Na fyzickom zariadení sa nesmieme prihlásiť do Apple ID Store nastavení, namiesto toho iOS pri prvom nákupe zobrazí prompt na sandbox login. Simulator používame len obmedzene. Pre unit testy StoreKit logiky je lepší StoreKit Configuration File priamo v Xcode, ktorý simuluje transakcie bez potreby siete a bez sandbox stavu. V .NET MAUI projektoch tento súbor nakonfigurujeme v Platforms/iOS/Info.plist pridaním SKAdNetworkItems a linkovaním Products.storekit.

Windows Store: sandbox APIs

Microsoft Store poskytuje StoreContext.GetDefault() API, ktoré vo vývojovom móde vracia mockované produkty. Pre plný test však potrebujeme aplikáciu asociovanú s Store cez Partner Center a signed MSIX package.

Testovacie scenáre, ktoré nesmiete vynechať

  • Reinštalácia aplikácie s aktívnou subscription: musí sa obnoviť bez užívateľského zásahu (Play) alebo cez Restore tlačidlo (App Store).
  • Prerušenie počas platby: používateľ zatvorí telefón v momente autorizácie. Pri ďalšom otvorení musí systém dobehnúť transakciu. Toto som sám v produkčnej appke prehliadol a zisťoval to hard way, keď mi klient poslal screenshot dvojitého odpočítania.
  • Refund: v sandboxe iOS vieme refund simulovať cez „Refund Test Purchase" v App Store Connect sandbox nastaveniach. Aplikácia musí correctly odobrať entitlement.
  • Downgrade / upgrade tier subscription: každá platforma to rieši inak; testujte oba smery.

Kedy siahnuť po RevenueCat alebo IAPHUB

V roku 2026 nemusíme robiť všetko sami. Existujú tri hlavné komerčné SDK, ktoré nad natívne API dávajú unifikovanú vrstvu, hostovanú validáciu a analytiku predplatných. Náš praktický rozhodovací rámec:

  • Vlastný backend + BillingService sample: ak už máme .NET backend s autentifikáciou a chceme mať plnú kontrolu. Náklady: 2 až 4 týždne inžiniera. Vhodné pre B2B, enterprise, high-margin produkty, kde 1 % z tržieb tretej strane bolí.
  • RevenueCat: najlepšia dashboardová analytika predplatných (retention cohorts, LTV, churn). Free do $2500 MTR (Monthly Tracked Revenue), potom 1 %. Vhodné pre B2C aplikácie s desiatkami tisíc predplatiteľov, kde produktový tím potrebuje rozhodovať na základe subscription metrík.
  • IAPHUB: čistá C# API, single-purpose, jednoduchšia integrácia než RevenueCat. Rovnaký cenový model. Vhodné pre menšie tímy, ktoré nepotrebujú pokročilú analytiku a chcú „hodiť SDK dnu a je to".
  • Adapty: silná v paywall A/B testovaní. Ak plánujeme systematicky optimalizovať konverziu na paywalle, oplatí sa.

Náš tím používa jednoduché pravidlo: ak očakávame tržby pod $200k ročne za nasledujúce 2 roky, ideme third-party SDK. Nad túto úroveň sa vlastný backend zaplatí. Toto číslo si každý tím musí prepočítať sám podľa nákladov na inžiniera, ale ako približná heuristika mi to funguje slušne aj mimo Slovenska.

Často kladené otázky

Je Plugin.InAppBilling od Jamesa Montemagna stále použiteľný v roku 2026?

Nie. Repozitár bol archivovaný v apríli 2025 a nepodporuje Google Play Billing Library 8, ktorá je od 31. augusta 2026 povinná pre nové publikácie a aktualizácie. Existujúce aplikácie v obchode budú fungovať ďalej, ale žiadny update už cez Play Console nepustíte.

Je Xamarin.Android.Google.BillingClient kompatibilný s .NET MAUI?

V .NET 10 áno, s určitými výhradami. Historicky mal balíček problémy s namespace resolutionom v čistom MAUI projekte (issue #30402), ale posledné verzie v roku 2026 tieto problémy prekryli. Overte si, že používate .NET 10 SDK a najnovšiu verziu balíčka pred nasadením do produkcie.

Ako implementovať tlačidlo „Obnoviť nákupy" v .NET MAUI?

Na Androide zavoláte BillingClient.QueryPurchasesAsync, ktorá vráti všetky aktívne, nespotrebované transakcie priamo z Google Play účtu (nie je potrebné žiadne používateľské potvrdenie). Na iOS zavoláte AppStore.SyncAsync(), ktoré vynúti sync s Apple ID a následne prejdete Transaction.CurrentEntitlements. Obe cesty by mali potom zaslať výsledky na váš backend na revalidáciu entitlementov.

Prečo je serverová validácia príjmov nutná aj v malých aplikáciách?

Pretože existujú verejne dostupné nástroje (Frida hooks, xposed moduly, jailbreak tweaky), ktoré do procesu injektnú falošný response z billing servisu. Aplikácia bez serverovej validácie tomu uverí a poskytne prémiový obsah zadarmo. Pri malých aplikáciách je pomer neplatiacich používateľov typicky 5 až 15 % a rastie priamoúmerne s hodnotou obsahu.

Kedy sa musím prejsť z Google Play Billing 7 na 8?

Do 31. augusta 2026 pre všetky nové aplikácie aj aktualizácie existujúcich aplikácií. Google umožňuje požiadať o predĺženie maximálne do 1. novembra 2026 v prípade preukázateľných technických prekážok. Po tomto termíne Play Console jednoducho odmietne prijať nový build s PBL 7. Odporúčame migrovať najneskôr v prvom kvartáli 2026.

Môžem testovať in-app nákupy v .NET MAUI simulátore?

Áno pre iOS cez StoreKit Configuration File, ktorý simuluje transakcie bez potreby sandbox účtu. Pre Android potrebujete fyzické zariadenie alebo emulátor so signed release APK a nakonfigurovaným license testerom v Play Console. Pre Windows funguje mock cez StoreContext v developer móde.

Priya Sharma
O Autorovi Priya Sharma

Cross-platform engineering lead who's shipped apps to millions on both Play Store and App Store. Believes shared codebases shouldn't mean shared mediocrity.