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();
}
}
}
As per official google-in-app-billing doc you should,
SkuDetailsParamswithQueryProductDetailsParamsBillingClient.querySkuDetailsAsync()call to useBillingClient.queryProductDetailsAsync()Update
querySkuDetails()functionAfter updating
queryProductDetailsAsyncyourmyProductSkuDetailsobject will change toProductDetailsPlease refer integrate-google-in-app-billing and Migration-to-goggle-play-billing-v6 official doc for detailed information.