Reputation: 118
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.
Upvotes: 7
Views: 3677
Reputation: 8016
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.
Upvotes: 0
Reputation: 1615
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:
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.
Upvotes: 0
Reputation: 118
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.
Upvotes: 1