Android - Google Play Billing Library error in production
Asked Answered
R

3

7

I have an Android app released on the Google Play Store, and last week I released a new update, just to fix some small issues. Starting with the day when I added the updated version on the Play Store, I could see on Firebase Crashlytics that there are issues when someone is trying to purchase an app feature.

Before I released the updated version in production, I added the app on the Alpha Testing so I can make sure that the InAppPurchase work, and it does.

When someone else is trying to purchase an app feature I can see that this Fatal Exception is thrown:

Fatal Exception: java.lang.IllegalArgumentException: SKU cannot be null.
   at com.android.billingclient.api.BillingFlowParams$Builder.build(com.android.billingclient:billing@@3.0.0:23)

The SKU's are still active on my "Managed Products" list.

This is the code that I use to initialize the billing client (within a fragment):

        billingClient = BillingClient.newBuilder(getActivity())
            .enablePendingPurchases()
            .setListener(purchasesUpdatedListener)
            .build();

This is the code that I use to start the connection:

billingClient.startConnection(new BillingClientStateListener() {
        @Override
        public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
            Log.d(TAG, "Connection finished");
            if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                // The BillingClient is ready. You can query purchases here.
                List<String> skuList = new ArrayList<>();
                skuList.add("unlock_keyboard");
                SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
                params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP);
                billingClient.querySkuDetailsAsync(params.build(),
                        new SkuDetailsResponseListener() {
                            @Override
                            public void onSkuDetailsResponse(@NonNull BillingResult billingResult,
                                                             List<SkuDetails> skuDetailsList) {
                                // Process the result.
                                if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && skuDetailsList != null) {
                                    for (Object skuDetailsObject : skuDetailsList) {
                                        skuDetails = (SkuDetails) skuDetailsObject;
                                        sku = skuDetails.getSku();


                                    }
                                    Log.d(TAG, "i got response");
                                    Log.d(TAG, String.valueOf(billingResult.getResponseCode()));
                                    Log.d(TAG, billingResult.getDebugMessage());
                                }
                            }
                        });
            }
        }

This is the code that I use to handle the purchase:

PurchasesUpdatedListener purchasesUpdatedListener = new PurchasesUpdatedListener() {
        @Override
        public void onPurchasesUpdated(@NonNull BillingResult billingResult, @Nullable List<Purchase> list) {
            if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && list != null) {
                for (Purchase purchase : list) {
                    handlePurchase(purchase);
                    Log.d(TAG, "Purchase completed" + billingResult.getResponseCode());
                }
            } else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED) {
                Log.d(TAG, "User Canceled" + billingResult.getResponseCode());
            } else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED) {
                if ("unlock_keyboard".equals(sku)) {
                    KeyboardAlreadyPurchasedConfirmation();
                }
                Log.d(TAG, "Item Already owned" + billingResult.getResponseCode());
            }
        }
    };

In order to launch the billing flow, the user must click on a button within a dialog. Here is the code:

        builder.setPositiveButton(
            getString(R.string.purchase_keyboard),
            new DialogInterface.OnClickListener() {
                public void onClick(DialogInterface dialog, int id) {

                    sku = "unlock_keyboard";
                    BillingFlowParams flowParams = BillingFlowParams.newBuilder()
                            .setSkuDetails(skuDetails)
                            .build();
                    billingClient.launchBillingFlow(Objects.requireNonNull(getActivity()), flowParams);

                }

            });

In the previous version of my app, this situation never happened, it just started after the new update. I'll just need to know what can cause this issue, could it be a problem with my code or just a issue with Google Play Services? I'll have to specify that this was happening on different devices with different Android versions.

Thanks a lot in advance.

Robeson answered 12/7, 2020 at 6:14 Comment(6)
remove .setSkuDetails(skuDetails) and add .setSku(YOUR_SKU) .setType(BillingClient.SkuType.INAPP) to your flowParamsFoolscap
do you have items on List<SkuDetails> skuDetailsList?Cimon
Do you have solved the issue? I have the same problem. For me it started to occur after I upgraded Google Play Billing library to version 3.0.Paranoiac
Unfortunately I did not solve the issue yet. I tried many different things like removing the app from some countries thinking that it might be just a problem with the Google Play Service that is no longer working for those countries but that solution did not work. The thing is that a few users were able to purchase the products after I upgraded Google Play Billing library to version 3.0. So this makes me think that the issue is not with my code, it's just an issue with the Google Play Billing library, an issue that no one from Google is informing us about, which is very disappointing.Robeson
any solution for this?Kiva
Unfortunately I couldn't find any solution to fix this issue. I tried different things, neither work. What's so sad is that these errors impact the user experience. The app crashes sometimes when users click on the "Purchase this item" button and because of that I start getting negative reviews. I'm thinking of removing the Google Billing Library from my app at least for now just to improve the user experience. If anyone will have an idea about how this issue can be fixed please leave a comment here. As I see, I am not the only one who is interested to find out how to fix this issueRobeson
R
1

I have not solved the issue yet but I found a way to reduce the numbers of errors generated by this library upgrade.

What I did was to downgrade the Google Billing Library from version 3.0.1 to version 2.1.0 and even though I still get some errors in Firebase (SKU is null), the majority of users are now able to purchase the products.

Also, I implemented a method that is called whenever the Google Billing library connection cannot be started when the activity is first opened, so more exactly this is restart billing connection method.

I would recommend you to try the same thing at least for now if you are experiencing the same issue because it seems that the Google Billing library still has some issues that need to be fixed.

1. In build.gradle(app) add this line:

implementation 'com.android.billingclient:billing:2.1.0'

2. Add the BILLING permission in AndroidManifest.xml file because the older versions of this library still require it:

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

3. Create a restart billing connection method:

public void restartBillingConnection() {
    billingClient = BillingClient.newBuilder(Objects.requireNonNull(getActivity())).enablePendingPurchases().setListener(ChooseOptionsFragment.this).build();

    billingClient.startConnection(new BillingClientStateListener() {@Override
    public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
        Log.d(TAG, "Connection finished");
        if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
            // The BillingClient is ready. You can query purchases here.
            List < String > skuList = new ArrayList < >();
            skuList.add(ITEM_SKU_AD_REMOVAL);
            SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
            params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP);
            billingClient.querySkuDetailsAsync(params.build(), new SkuDetailsResponseListener() {@Override
            public void onSkuDetailsResponse(@NonNull BillingResult billingResult, List < SkuDetails > skuDetailsList) {
                if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && skuDetailsList != null) {
                    for (Object skuDetailsObject: skuDetailsList) {
                        skuDetails = (SkuDetails) skuDetailsObject;
                        sku = skuDetails.getSku();
                        String price = skuDetails.getPrice();
                        if (ITEM_SKU_AD_REMOVAL.equals(sku)) {
                            skuPrice = price;
                            BillingFlowParams flowParams = BillingFlowParams.newBuilder().setSkuDetails(skuDetails).build();
                            billingClient.launchBillingFlow(Objects.requireNonNull(Objects.requireNonNull(getActivity())), flowParams);
                        }
                        else {
                            Log.d(TAG, "Sku is null");
                        }

                    }
                    Log.d(TAG, "i got response");
                    Log.d(TAG, String.valueOf(billingResult.getResponseCode()));
                    Log.d(TAG, billingResult.getDebugMessage());
                }
                else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.ERROR) {
                    Toast.makeText(getActivity(), "Error in completing the purchase!", Toast.LENGTH_SHORT).show();
                }
            }
            });
        }

        else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.SERVICE_TIMEOUT) {
            Toast.makeText(getActivity(), "Service timeout!", Toast.LENGTH_SHORT).show();
        }
        else {
            Toast.makeText(getActivity(), "Failed to connect to the billing client!", Toast.LENGTH_SHORT).show();
        }

    }@Override
    public void onBillingServiceDisconnected() {
        restartBillingConnection();
    }
    });
}

4. Make sure that this method is called when the Google Billing service gets disconnected:

@Override
    public void onBillingServiceDisconnected() {
        restartBillingConnection();
    }

Hope that this solution will help you to fix the issue for now. If you will have another way to have it completely fixed please leave an answer in this post.

Robeson answered 31/10, 2020 at 13:41 Comment(0)
P
0

I had the same or maybe just a similar problem. But I have found the reason in my case. I could reproduce it in the following way:

  • Disable internet connection
  • Reinstall the app
  • Try to start the In-App purchase like described here: Link
    • Seems to be similar to your routine

The querySkuDetailsAsync returned null for every item and when initiatePurchaseFlow is called null is passed to it and the BillingFlowParams.Builder. I assume that that has changed and before it did not throw the Exception but just handled it differently. I fixed this now by checking if the item in the Map is null and then I display a warning that an Internet connection is required.

Paranoiac answered 3/12, 2020 at 12:5 Comment(0)
M
0

I believe this may be occurring due to concurrency (ie: buy button being hit twice).

Try disabling the "buy" button while the whole billing flow is running.

Merilyn answered 20/1, 2022 at 2:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.