My app is supposed to perform a simple subscription via Google Play Service once User clicks on the Subscribe button.
Please advice what's wrong?
Entire subscription logic is sitting in a single class.
I've done all the necessarycsteps from the Google Play Console side: single subscription id, single plan id, single offer id. Should I use any other id besides subscription id in the code?
Opening of a Subscription dialog returns 'BILLING_UNAVAILABLE'.
Clicking on a Subscribe button returns 'launchBillingFlow: BillingClient is not ready'.
Same actions on a mobile phone do nothing.
Internet connection is ok as the AdMob testing ad works fine.
package com.something.something;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import androidx.lifecycle.MutableLiveData;
import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingClientStateListener;
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.ProductDetails;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.PurchasesUpdatedListener;
import com.android.billingclient.api.QueryProductDetailsParams;
import com.android.billingclient.api.QueryPurchasesParams;
import com.google.common.collect.ImmutableList;
import java.util.List;
public class MenuSubscription extends DialogFragment implements BillingClientStateListener {
private static final String TAG = "MenuSubscription";
private static final String SUBSCRIPTION_SKU = "unique_subscription_id_created_via_google_play_console";
public SingleLiveEvent<List<Purchase>> purchaseUpdateEvent = new SingleLiveEvent<>();
public MutableLiveData<List<Purchase>> purchases = new MutableLiveData<>();
public MutableLiveData<ProductDetails> skuDetailsLiveData = new MutableLiveData<>();
private BillingClient billingClient;
private final PurchasesUpdatedListener purchasesUpdatedListener = (billingResult, purchases) -> {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && purchases != null) {
processPurchases(purchases);
} else {
handleAllBillingErrors(billingResult);
}
};
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
setupBillingClient();
}
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
View view = requireActivity().getLayoutInflater().inflate(R.layout.subscription, null);
Button button_subscribe = view.findViewById(R.id.subscribe_button);
button_subscribe.setOnClickListener(v -> launchBillingFlow());
return new AlertDialog.Builder(requireActivity())
.setView(view)
.create();
}
private void handleAllBillingErrors(BillingResult billingResult){
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK){
showMessage("SUCCESS");
} else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED) {
showMessage("User cancelled the purchase after showing plans to the user");
} else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED) {
showMessage("Feature not supported on this phone");
} else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.DEVELOPER_ERROR) {
showMessage("DEVELOPER_ERROR");
} else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.BILLING_UNAVAILABLE) {
showMessage("BILLING_UNAVAILABLE");
} else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED) {
showMessage("ITEM_ALREADY_OWNED");
} else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.ITEM_NOT_OWNED) {
showMessage("ITEM_NOT_OWNED");
} else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.ITEM_UNAVAILABLE) {
showMessage("ITEM_UNAVAILABLE");
} else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.SERVICE_DISCONNECTED) {
showMessage("SERVICE_DISCONNECTED");
} else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE) {
showMessage("SERVICE_UNAVAILABLE");
} else {
showMessage("Something went wrong while setting up purchase after showing plans to the user");
}
}
@Override
public void onDismiss(@NonNull DialogInterface dialog) {
super.onDismiss(dialog);
destroy();
}
public void destroy() {
Log.d(TAG, "ON_DESTROY");
if (billingClient.isReady()) {
Log.d(TAG, "BillingClient can only be used once -- closing connection");
showMessage("BillingClient can only be used once -- closing connection");
billingClient.endConnection();
}
}
@Override
public void onBillingSetupFinished(BillingResult billingResult) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
queryProductDetails();
queryPurchases();
}
}
@Override
public void onBillingServiceDisconnected() {
Log.w(TAG, "Billing service disconnected. Retrying with exponential backoff...");
showMessage("Billing service disconnected. Retrying with exponential backoff...");
}
public void onSkuDetailsResponse(@NonNull BillingResult billingResult, @Nullable List<ProductDetails> productDetailsList) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
if (productDetailsList != null && !productDetailsList.isEmpty()) {
ProductDetails productDetails = productDetailsList.get(0);
skuDetailsLiveData.postValue(productDetails);
}
}
}
public void queryPurchases() {
if (!billingClient.isReady()) {
Log.e(TAG, "queryPurchases: BillingClient is not ready");
showMessage("queryPurchases: BillingClient is not ready");
}
QueryPurchasesParams queryPurchasesParams = QueryPurchasesParams.newBuilder()
.setProductType(BillingClient.ProductType.SUBS)
.build();
billingClient.queryPurchasesAsync(queryPurchasesParams, (billingResult, purchasesList) -> {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
processPurchases(purchasesList);
} else {
handleAllBillingErrors(billingResult);
}
});
}
private void launchBillingFlow() {
if (!billingClient.isReady()) {
Log.e(TAG, "launchBillingFlow: BillingClient is not ready");
showMessage("launchBillingFlow: BillingClient is not ready");
return;
}
queryProductDetails();
}
private void processPurchases(List<Purchase> purchasesList) {
if (purchasesList != null) {
Log.d(TAG, "processPurchases: " + purchasesList.size() + " purchase(s)");
showMessage("processPurchases: " + purchasesList.size() + " purchase(s)");
} else {
Log.d(TAG, "processPurchases: with no purchases");
showMessage("processPurchases: with no purchases");
}
purchaseUpdateEvent.postValue(purchasesList);
purchases.postValue(purchasesList);
if (purchasesList != null) {
logAcknowledgementStatus(purchasesList);
}
}
private void logAcknowledgementStatus(@NonNull List<Purchase> purchasesList) {
int ack_yes = 0;
int ack_no = 0;
for (Purchase purchase : purchasesList) {
if (purchase.isAcknowledged()) {
ack_yes++;
} else {
ack_no++;
}
}
Log.d(TAG, "logAcknowledgementStatus: acknowledged=" + ack_yes + " unacknowledged=" + ack_no);
showMessage("logAcknowledgementStatus: acknowledged=" + ack_yes + " unacknowledged=" + ack_no);
}
public void queryProductDetails() {
ImmutableList<QueryProductDetailsParams.Product> productList = ImmutableList.of(QueryProductDetailsParams.Product.newBuilder()
.setProductId(SUBSCRIPTION_SKU)
.setProductType(BillingClient.ProductType.SUBS)
.build());
QueryProductDetailsParams params = QueryProductDetailsParams.newBuilder()
.setProductList(productList)
.build();
billingClient.queryProductDetailsAsync(params, (billingResult, productDetailsList) -> {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
if (!productDetailsList.isEmpty()) {
onSkuDetailsResponse(billingResult, productDetailsList);
}
} else {
handleAllBillingErrors(billingResult);}
});
}
private BillingClient initializeBillingClient() {
BillingClient billingClient = BillingClient.newBuilder(requireContext())
.setListener(purchasesUpdatedListener)
.enablePendingPurchases()
.build();
billingClient.startConnection(new BillingClientStateListener() {
@Override
public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
// BillingClient is ready, you can now launch the billing flow
launchBillingFlow();
} else {
handleAllBillingErrors(billingResult);}
}
@Override
public void onBillingServiceDisconnected() {
// Retry or handle disconnection as needed
Log.e(TAG, "Billing service disconnected. Retrying with exponential backoff...");
showMessage("Billing service disconnected. Retrying with exponential backoff...");
}
});
return billingClient;
}
public void setupBillingClient() {
billingClient = initializeBillingClient();
billingClient = BillingClient.newBuilder(requireContext()).setListener(purchasesUpdatedListener).enablePendingPurchases().setListener(purchasesUpdatedListener).build();
billingClient.startConnection(new BillingClientStateListener() {
@SuppressLint("SuspiciousIndentation")
@Override
public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
handleAllBillingErrors(billingResult);
queryProductDetails();
} else
handleAllBillingErrors(billingResult);
}
@Override
public void onBillingServiceDisconnected() {
Log.e(TAG, "Billing service disconnected. Retrying with exponential backoff...");
showMessage("Billing service disconnected. Retrying with exponential backoff...");
}
});
}
private void showMessage(String message) {
Toast.makeText(getActivity(), message, Toast.LENGTH_SHORT).show();
}
}