Android Studio - In App Purchase: billingclient problem

510 Views Asked by At

A few years ago I made a app for Android, but the project was deleted by a mistake. After now a few more years I decided to write it again. So I have came to the part where I want to add in app purchase (remove ads), but somehow it doesn't work as planned. I have tried following the docs and searching online, but no luck.

I have these this in my app build.gradle:

dependencies {

    implementation("androidx.appcompat:appcompat:1.6.1")
    implementation("com.google.android.material:material:1.10.0")
    implementation("androidx.constraintlayout:constraintlayout:2.1.4")
    implementation("androidx.test.espresso:espresso-core:3.5.1")

    val billing_version = "6.1.0"

    implementation("com.android.billingclient:billing:$billing_version")
    implementation("com.android.billingclient:billing-ktx:$billing_version")

    testImplementation("junit:junit:4.13.2")
    androidTestImplementation("androidx.test.ext:junit:1.1.5")
    androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
}

This is what my Java Class look like:

import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingClientStateListener;
import com.android.billingclient.api.BillingFlowParams;
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.PurchasesUpdatedListener;
import com.android.billingclient.api.SkuDetails;
import com.android.billingclient.api.SkuDetailsParams;

import java.util.Arrays;
import java.util.List;
import java.util.Locale;

public class InfoController extends AppCompatActivity implements PurchasesUpdatedListener {

    private Button upgradeButton;

    // Billing variables
    private BillingClient billingClient;
    private PurchasesUpdatedListener purchasesUpdatedListener;

    private SkuDetails myProductSkuDetails; // Define SkuDetails field

    List<String> skuList = Arrays.asList("com.xxxxxxx.yyyyyyyyyyyyy.pro");


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_info);

        upgradeButton = findViewById(R.id.upgradeButton);

        billingClient = BillingClient.newBuilder(this)
                .setListener(this)
                .enablePendingPurchases()
                .build();

        billingClient.startConnection(new BillingClientStateListener() {
            @Override
            public void onBillingSetupFinished(BillingResult billingResult) {
                if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                    // The billing client is ready, query SkuDetails here
                    querySkuDetails();
                } else {
                    // Handle the error
                    Toast.makeText(InfoController.this, "Billing setup failed: " + billingResult.getDebugMessage(), Toast.LENGTH_SHORT).show();
                }
            }

            @Override
            public void onBillingServiceDisconnected() {
                // Handle the case when the billing service is disconnected
                Toast.makeText(InfoController.this, "Billing service disconnected", Toast.LENGTH_SHORT).show();
            }
        });

    }

    private void querySkuDetails() {
        // Query SkuDetails for your product
        List<String> skuList = Arrays.asList("com.xxxxxxx.yyyyyyyyyyyyy.pro");
        SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
        params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP);

        Log.d("BillingDebug", "Before querySkuDetailsAsync");
        billingClient.querySkuDetailsAsync(params.build(), (billingResult, skuDetailsList) -> {
            Log.d("BillingDebug", "Inside querySkuDetailsAsync callback");
            if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && skuDetailsList != null) {
                // Use the first SkuDetails object
                Toast.makeText(this, "Billing OK", Toast.LENGTH_SHORT).show();
                myProductSkuDetails = skuDetailsList.get(0);
            } else {
                Toast.makeText(this, "Failed to retrieve SKU details", Toast.LENGTH_SHORT).show();
            }
        });
        Log.d("BillingDebug", "After querySkuDetailsAsync");

    }

    @Override
    public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) {
        // Implement your logic here when purchases are updated
        if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && purchases != null) {
            for (Purchase purchase : purchases) {
                // Handle the purchase
                Toast.makeText(this, "OK", Toast.LENGTH_SHORT).show();
            }
        } else {
            // Handle an error
            Toast.makeText(this, "ERROR", Toast.LENGTH_SHORT).show();
        }
    }

    public void onUpgradeButtonClick(View view) {
        if (myProductSkuDetails != null) {
            Toast.makeText(this, "Not null", Toast.LENGTH_SHORT).show();
            // Create a BillingFlowParams object
            BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
                    .setSkuDetails(myProductSkuDetails)
                    .build();

            // Launch the billing flow
            BillingResult result = billingClient.launchBillingFlow(this, billingFlowParams);
            if (result.getResponseCode() != BillingClient.BillingResponseCode.OK) {
                // Handle the error


            }
        } else {
            // SkuDetails not available, handle accordingly

            Toast.makeText(this, "SkuDetails not available", Toast.LENGTH_SHORT).show();
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (billingClient != null) {
            billingClient.endConnection();
        }
    }
}

Issue:

So when I load this activity, it opens fine, no error and no Toast message. But when I press onUpgradeButtonClick, it gives me the Toast message: SkuDetails not available.

I am running this on Simulator Pixel 7 API 34 with Google Play Store support, and I am logged in to the Google Play Store.

Any suggestions?

CODE UPDATE BELOW:

So I tried a different approach. I have been following a YouTube video. For testing, the upgradeButton actually shows the in app purchase name now, but nothing happens when pressing the upgradeButton. And no Toast is shown on launch or press, and iapTextView text does not show anything either. This is what I got:

package com.xxxxxxx.yyyyyyyyyyyyy.droid;

import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.test.espresso.core.internal.deps.guava.collect.ImmutableList;

import com.android.billingclient.api.AcknowledgePurchaseParams;
import com.android.billingclient.api.AcknowledgePurchaseResponseListener;
import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingClientStateListener;
import com.android.billingclient.api.BillingFlowParams;
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.ConsumeParams;
import com.android.billingclient.api.ConsumeResponseListener;
import com.android.billingclient.api.ProductDetails;
import com.android.billingclient.api.ProductDetailsResponseListener;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.PurchasesUpdatedListener;
import com.android.billingclient.api.QueryProductDetailsParams;
import com.android.billingclient.api.SkuDetails;
import com.android.billingclient.api.SkuDetailsParams;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class InfoController extends AppCompatActivity {

    private Button upgradeButton;
    private TextView iapTextView;

    // Billing variables
    private BillingClient billingClient;
    String subsName,des;
    Boolean isSuccess = false;
    Boolean isPro = false;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_info);

        upgradeButton = findViewById(R.id.upgradeButton);
        iapTextView = findViewById(R.id.iapTextView);

        iapTextView.setVisibility(View.VISIBLE);
        iapTextView.setTextColor(getResources().getColor(R.color.white));
        iapTextView.setBackgroundColor(getResources().getColor(R.color.blue));

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

        getPrice();
    }

    private PurchasesUpdatedListener purchasesUpdatedListener = new PurchasesUpdatedListener() {
        @Override
        public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) {
            // To be implemented in a later section.

            if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && purchases != null) {
                for (Purchase purchase : purchases) {
                    handlePurchase(purchase);
                }
            } else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED){
                Toast.makeText(InfoController.this, "ITEM_ALREADY_OWNED", Toast.LENGTH_SHORT).show();
                iapTextView.setText("ITEM_ALREADY_OWNED");
            } else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED) {
                Toast.makeText(InfoController.this, "FEATURE_NOT_SUPPORTED", Toast.LENGTH_SHORT).show();
                iapTextView.setText("FEATURE_NOT_SUPPORTED");
            } else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.BILLING_UNAVAILABLE) {
                Toast.makeText(InfoController.this, "BILLING_UNAVAILABLE", Toast.LENGTH_SHORT).show();
                iapTextView.setText("BILLING_UNAVAILABLE");
            } else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED) {
                Toast.makeText(InfoController.this, "USER_CANCELED", Toast.LENGTH_SHORT).show();
                iapTextView.setText("USER_CANCELED");
            } else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.DEVELOPER_ERROR) {
                Toast.makeText(InfoController.this, "DEVELOPER_ERROR", Toast.LENGTH_SHORT).show();
                iapTextView.setText("DEVELOPER_ERROR");
            } else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.ITEM_UNAVAILABLE) {
                Toast.makeText(InfoController.this, "ITEM_UNAVAILABLE", Toast.LENGTH_SHORT).show();
                iapTextView.setText("ITEM_UNAVAILABLE");
            } else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.NETWORK_ERROR) {
                Toast.makeText(InfoController.this, "NETWORK_ERROR", Toast.LENGTH_SHORT).show();
                iapTextView.setText("NETWORK_ERROR");
            } else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.SERVICE_DISCONNECTED) {
                Toast.makeText(InfoController.this, "SERVICE_DISCONNECTED", Toast.LENGTH_SHORT).show();
                iapTextView.setText("SERVICE_DISCONNECTED");
            } else {
                Toast.makeText(InfoController.this, "Error: " + billingResult.getDebugMessage(), Toast.LENGTH_SHORT).show();
            }
        }
    };

    void handlePurchase(final Purchase purchase) {
        ConsumeParams consumeParams =
                ConsumeParams.newBuilder()
                        .setPurchaseToken(purchase.getPurchaseToken())
                        .build();

        ConsumeResponseListener listener = (billingResult, s) -> {

            if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {

            }
        };

        billingClient.consumeAsync(consumeParams,listener);

        if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
            if (!purchase.isAcknowledged()) {
                AcknowledgePurchaseParams acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
                        .setPurchaseToken(purchase.getPurchaseToken())
                        .build();
                billingClient.acknowledgePurchase(acknowledgePurchaseParams,acknowledgePurchaseResponseListener);
                iapTextView.setText("You have purchased PRO");
            } else {
                iapTextView.setText("Already purchased!");
            }
        } else if (purchase.getPurchaseState() == Purchase.PurchaseState.PENDING) {
            iapTextView.setText("PURCHASE PENDING");
        } else if (purchase.getPurchaseState() == Purchase.PurchaseState.UNSPECIFIED_STATE) {
            iapTextView.setText("UNSPECIFIED_STATE");
        }
    }

    AcknowledgePurchaseResponseListener acknowledgePurchaseResponseListener = new AcknowledgePurchaseResponseListener() {
        @Override
        public void onAcknowledgePurchaseResponse(@NonNull BillingResult billingResult) {
            if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                iapTextView.setText("You just got PRO!");
            }
        }
    };

    private boolean verifyValidSignature(String signedData, String signature) {
        return  Security.verifyPurchase(signedData, signature);
        /*
        try {
            // String base64Key = "";
            // return Security.verifyPurchase(base64Key, signedData, signature);
            return  Security.verifyPurchase(signedData, signature);
        } catch (IOException e) {
            return false;
        }
        */
    }

    private void getPrice() {
        billingClient.startConnection(new BillingClientStateListener() {
            @Override
            public void onBillingSetupFinished(BillingResult billingResult) {
                if (billingResult.getResponseCode() ==  BillingClient.BillingResponseCode.OK) {
                    // The BillingClient is ready. You can query purchases here.

                    ExecutorService executorService = Executors.newSingleThreadExecutor();
                    executorService.execute(new Runnable() {
                        @Override
                        public void run() {

                            List<QueryProductDetailsParams.Product> productList = new ArrayList<>();
                            QueryProductDetailsParams.Product product = QueryProductDetailsParams.Product.newBuilder()
                                    .setProductId("com.xxxxxxx.yyyyyyyyyyyyy.pro")
                                    .setProductType(BillingClient.ProductType.INAPP)
                                    .build();
                            productList.add(product);
                            QueryProductDetailsParams queryProductDetailsParams =
                                    QueryProductDetailsParams.newBuilder()
                                            .setProductList(productList)
                                            .build();


                            billingClient.queryProductDetailsAsync(
                                    queryProductDetailsParams,
                                    new ProductDetailsResponseListener() {
                                        public void onProductDetailsResponse(BillingResult billingResult,
                                                                             List<ProductDetails> productDetailsList) {
                                            for (ProductDetails productDetails:productDetailsList) {
                                                String productID = productDetails.getProductId();

                                                subsName = productDetails.getName();
                                                des = productDetails.getDescription();

                                                String formattedPrice = productDetails.getSubscriptionOfferDetails().get(0).getPricingPhases().getPricingPhaseList().get(0).getFormattedPrice();

                                            }
                                        }
                                    }
                            );
                        }
                    });
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            try {
                                Thread.sleep(1000);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }

                            upgradeButton.setText("Name: "+subsName);

                        }
                    });
                }
            }
            @Override
            public void onBillingServiceDisconnected() {
                // Try to restart the connection on the next request to
                // Google Play by calling the startConnection() method.
            }
        });
    }



    public void onUpgradeButtonClick(View view) {
        billingClient.startConnection(new BillingClientStateListener() {
            @Override
            public void onBillingServiceDisconnected() {

            }

            @Override
            public void onBillingSetupFinished(@NonNull BillingResult billingResult) {

                Toast.makeText(InfoController.this, "Pressed", Toast.LENGTH_SHORT).show();
                QueryProductDetailsParams queryProductDetailsParams = QueryProductDetailsParams.newBuilder().setProductList(
                        Arrays.asList(QueryProductDetailsParams.Product.newBuilder()
                                .setProductId("com.xxxxxxx.yyyyyyyyyyyyy.pro")
                                .setProductType(BillingClient.ProductType.INAPP)
                                .build())
                ).build();

                billingClient.queryProductDetailsAsync(
                        queryProductDetailsParams,
                        new ProductDetailsResponseListener() {
                            public void onProductDetailsResponse(BillingResult billingResult,
                                                                 List<ProductDetails> productDetailsList) {
                                for (ProductDetails productDetails : productDetailsList) {
                                    String offerToken = productDetails.getSubscriptionOfferDetails().get(0).getOfferToken();

                                    // Create a list with a single element
                                    List<BillingFlowParams.ProductDetailsParams> paramsList = Arrays.asList(
                                            BillingFlowParams.ProductDetailsParams.newBuilder()
                                                    .setProductDetails(productDetails)
                                                    .setOfferToken(offerToken)
                                                    .build()
                                    );

                                    // Use the list in BillingFlowParams
                                    BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
                                            .setProductDetailsParamsList(paramsList)
                                            .build();

                                    billingClient.launchBillingFlow(InfoController.this, billingFlowParams);
                                }
                            }
                        }
                );
            }
        });
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (billingClient != null) {
            billingClient.endConnection();
        }
    }
}
1

There are 1 best solutions below

12
On

As per official google-in-app-billing doc you should,

  • Replace SkuDetailsParams with QueryProductDetailsParams
  • Switch the BillingClient.querySkuDetailsAsync() call to use BillingClient.queryProductDetailsAsync()

Update querySkuDetails() function

 private void querySkuDetails() {
    // QueryProductDetailsParams for your product
    QueryProductDetailsParams.Product inAppProduct = QueryProductDetailsParams.Product.newBuilder()
            .setProductId("com.xxxxxxx.yyyyyyyyyyyyy.pro")
            .setProductType(BillingClient.ProductType.INAPP)
            .build();
    List<QueryProductDetailsParams.Product> productList = Arrays.asList(inAppProduct);
    QueryProductDetailsParams params = QueryProductDetailsParams.newBuilder()
            .setProductList(productList)
            .build();
    billingClient.queryProductDetailsAsync(params, (billingResult, productDetailsList) -> {
        // productDetailsList will be List<ProductDetails>
        if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && !productDetailsList.isEmpty()) {
            // Use the first productDetailsList object
            Toast.makeText(this, "Billing OK", Toast.LENGTH_SHORT).show();
            myProductSkuDetails = productDetailsList.get(0);
        } else {
            Toast.makeText(this, "Failed to retrieve SKU details", Toast.LENGTH_SHORT).show();
        }
    });
}

After updating queryProductDetailsAsync your myProductSkuDetails object will change to ProductDetails

 private ProductDetails myProductSkuDetails; // Define SkuDetails field

Please refer integrate-google-in-app-billing and Migration-to-goggle-play-billing-v6 official doc for detailed information.