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,
SkuDetailsParams
withQueryProductDetailsParams
BillingClient.querySkuDetailsAsync()
call to useBillingClient.queryProductDetailsAsync()
Update
querySkuDetails()
functionAfter updating
queryProductDetailsAsync
yourmyProductSkuDetails
object will change toProductDetails
Please refer integrate-google-in-app-billing and Migration-to-goggle-play-billing-v6 official doc for detailed information.