Table of Contents

    Book an Appointment

    INTRODUCTION: How Did We Discover the Empty Product List Issue?

    While working on a cross-platform SaaS platform for the health and wellness industry, we needed to implement a seamless in-app purchase flow to unlock premium features. Our technology stack relied on Avalonia UI to deliver a unified experience across desktop and mobile, utilizing .NET bindings for native device integrations.

    During the integration of the Android payment layer, we encountered a perplexing situation. We were implementing a one-time purchase model using the native billing APIs. The connection to the store was successful and our product was actively configured in the Play Console. However, whenever we queried the store for our premium upgrade, the API consistently returned an empty product list. There were no exceptions, no error codes—just a silent failure to retrieve the active product.

    In production environments, payment gateway failures directly impact revenue. A missing purchase option means users cannot upgrade, leading to immediate business losses. This challenge required us to dive deep into how cross-platform frameworks interact with native Android services. We are sharing this experience so other engineering teams integrating google play billing can avoid the same deployment hurdles.

    PROBLEM CONTEXT: Where Did the Architecture Fail?

    The business use case was straightforward: allow free-tier users to purchase a lifetime “Pro” license. To achieve this in our Avalonia (11.x) Android application, we utilized the Xamarin.Android.Google.BillingClient package (Version 9.1.0.1).

    In the Google Play Console, we created a one-time product with the ID pro_upgrade. We ensured the buying option was marked as active across 173 countries and that legacy mode was activated. The application ID matched the package ID in the store exactly.

    Architecturally, we abstracted the billing logic into a dedicated service layer (InAppPurchaseService). This service was responsible for initializing the billing client, ensuring a connection to the Google Play service on the device, querying available products and presenting the localized pricing to the Avalonia UI layer. The architecture was sound, but the integration layer between the .NET environment and the Play Store backend was failing to yield the expected data.

    WHAT WENT WRONG: Why Was Google Play Billing Returning Zero Products?

    When we ran the application on both emulators and physical devices using a closed testing track, the UI failed to render the purchase button. We immediately inspected the device logs and found the following telemetry:

    [WRN] InAppPurchaseService - Premium product pro_upgrade not found
    [INF] InAppPurchaseService - QueryProductDetailsAsync: ResponseCode="Ok", DebugMessage=, ProductCount=0, UnfetchedCount=0
    [INF] InAppPurchaseService - Querying product: pro_upgrade, ListCount=1, PackageName=com.generic.saas.app, ProductType=inapp
    [INF] InAppPurchaseService - PRODUCT_DETAILS supported: "Ok", DebugMessage=
    

    The logs revealed a contradictory state:

    • The API reported that PRODUCT_DETAILS was supported and the response code was Ok.
    • The API acknowledged our query for the product type inapp.
    • However, ProductCount was 0 and the product did not appear in the UnfetchedCount list either.

    We even deleted the Play Store cache on the test devices and re-authenticated the test user, but the product simply would not retrieve. It was a classic “black box” scenario where the API dictates success but delivers no payload.

    HOW WE APPROACHED THE SOLUTION: What Diagnostic Steps Did We Take?

    When dealing with native API bindings in cross-platform frameworks, the root cause can lie in the code, the environment or the remote configuration. We methodically tested several hypotheses.

    Did We Misconfigure the Product Types?

    Our first assumption was a mismatch in the product type query. Google Play Billing distinguishes heavily between subscriptions (Subs) and one-time purchases (Inapp). We reviewed our QueryProductDetailsParams builder to ensure we were explicitly requesting BillingClient.ProductType.Inapp. The code was correct, ruling out a simple query syntax error.

    Was the Play Store Cache Causing Stale Data?

    It is a known issue that the Google Play Store app caches billing configurations aggressively. We suspected the test device was relying on a stale cache from before the pro_upgrade product was published. We cleared the cache and data of the Google Play Store app and Google Play Services, then restarted the device. This approach is standard practice, but in this instance, it yielded no change.

    Could It Be an API Binding Issue in .NET?

    We considered whether the specific version of Xamarin.Android.Google.BillingClient had a bug in how it mapped Java lists to .NET collections. We temporarily downgraded the package and wrote a minimal reproduction script. The results were identical, confirming the binding library was functioning as intended and the issue resided elsewhere.

    Were the Testing Environments Out of Sync?

    This led us to the most complex aspect of Android monetization: environment synchronization. We realized that while our code was flawless, Google’s backend requires a strict cryptographic and identity match before it will return product data for unreleased apps.

    FINAL IMPLEMENTATION: How Did We Resolve the Empty Product List?

    The resolution did not require changing our query logic, which was already optimal. Instead, we had to align four critical environmental pillars that govern how google play billing validates requests.

    Here is the exact implementation checklist we enforced to resolve the issue:

    1. Strict Keystore Matching:

    Even when testing locally or on an emulator, the APK deployed via the IDE must be signed with the exact same Keystore used to sign the bundle uploaded to the Google Play Console Closed Testing track. Debug keystores will result in a silent ProductCount=0.

    2. Version Code Synchronization:

    The VersionCode of the application running on the device must be equal to or greater than the version code of the release active in the testing track. We automated our CI/CD pipeline to ensure local deployments matched the active track version.

    3. License Tester Verification:

    The Google account logged into the test device must be explicitly added to the “License Testing” list in the Play Console under Setup > License Testing, not just the testing track email list. Furthermore, the testing track must be fully rolled out (approved by Google’s automated review).

    Once these environmental configurations were synchronized, our original C# code worked perfectly. For reference, here is the sanitized, production-ready implementation of the query method that successfully retrieved the products:

    public async Task RefreshProductInfoAsync()
    {
        try
        {
            await EnsureBillingConnectedAsync();
            var product = QueryProductDetailsParams.Product
                .NewBuilder()
                .SetProductId(PremiumProductId)
                .SetProductType(BillingClient.ProductType.Inapp)
                .Build();
            var productList = new List<QueryProductDetailsParams.Product> { product };
            var result = await _billingClient!.QueryProductDetailsAsync(
                QueryProductDetailsParams.NewBuilder()
                    .SetProductList(productList)
                    .Build());
            if (result.Result.ResponseCode != BillingResponseCode.Ok)
            {
                Log.Warning("InAppPurchaseService - Query failed: {ResponseCode}", result.Result.ResponseCode);
                return;
            }
            _premiumProduct = result.ProductDetailsList?
                .FirstOrDefault(x => x.ProductId == PremiumProductId);
            if (_premiumProduct is null)
            {
                Log.Warning("InAppPurchaseService - Premium product not found");
                return;
            }
            var offers = _premiumProduct.OneTimePurchaseOfferDetailsList;
            
            if (offers != null && offers.Count > 0)
            {
                _premiumOffer = offers.First();
                ProductInfo = new StoreProductInfo
                {
                    Title = _premiumProduct.Title,
                    FormattedPrice = _premiumOffer.FormattedPrice
                };
            }
        }
        catch (Exception ex)
        {
            Log.Error(ex, "InAppPurchaseService - Initialization failed");
        }
    }
    

    Additionally, we ensured the Android Manifest explicitly declared the required permission, which is sometimes stripped during aggressive linking processes in cross-platform builds:

    <uses-permission android:name="com.android.vending.BILLING" />
    

    LESSONS FOR ENGINEERING TEAMS: What Can You Learn From This?

    For organizations looking to hire dotnet developers for cross-platform frameworks, it is crucial to ensure the team understands not just the code, but the native ecosystem constraints. Here are the key takeaways:

    • Code Correctness is Not Enough: In native app monetization, environmental parity (Signing, Versioning, Authentication) is just as critical as the syntax of your API calls.
    • Never Test Billing on Debug Builds: Always configure your IDE to use the release Keystore when testing Google Play integrations to avoid phantom API responses.
    • Understand License Testing: Adding a user to a closed testing track does not automatically grant them billing sandbox access. The “License Testers” section in the Play Console is a separate, mandatory configuration.
    • Implement Robust Telemetry: The logging structure we used (capturing ResponseCode, ProductCount and UnfetchedCount) was vital. Without it, we would not have known that the API call was actually succeeding but returning empty data.
    • Expect Propagation Delays: When creating a new product in the Play Console, it can take up to 24 hours for it to become queryable across Google’s global infrastructure, even for test accounts.

    WRAP UP: Final Thoughts on App Monetization

    Integrating monetization layers into cross-platform applications requires a meticulous approach to both software architecture and vendor platform configuration. By ensuring strict alignment between local testing environments and Google Play’s cryptographic requirements, we were able to successfully deploy our one-time purchase model. When companies look to hire software developer talent, they need engineers who can navigate these frustrating “black box” scenarios with logical debugging steps rather than random guesswork.

    If your organization is struggling with complex native integrations or looking to hire android developers for enterprise solutions, our experienced engineering teams can help you architect, debug and deliver resilient software. Feel free to contact us to discuss your next technical initiative.

    Social Hashtags

    #GooglePlayBilling #AndroidDevelopment #InAppPurchases #DotNet #AvaloniaUI #GooglePlayConsole #BillingClient #AndroidBilling #AppMonetization #DotNetDevelopment #MobileAppDevelopment #Debugging

    Frequently Asked Questions