I need some input here on how connection is made and querying the sku details. I'm working on the tutorial and copying the in app billing logic over to my app.
https://codelabs.developers.google.com/codelabs/play-billing-codelab
I followed the tutorial without any issues. The issue lies in how the connection is made and then querying the sku details.
When I made an instance of BillingManager class, it'll attempt to make a connection -
public BillingManager(Activity activity) {
mActivity = activity;
mBillingClient = BillingClient.newBuilder(mActivity).setListener(this).build();
mBillingClient.startConnection(new BillingClientStateListener() {
#Override
public void onBillingSetupFinished(#BillingClient.BillingResponse int billingResponse) {
if (billingResponse == BillingClient.BillingResponse.OK) {
Log.i(TAG, "onBillingSetupFinished() response: " + billingResponse);
} else {
Log.w(TAG, "onBillingSetupFinished() error code: " + billingResponse);
}
}
#Override
public void onBillingServiceDisconnected() {
Log.w(TAG, "onBillingServiceDisconnected()");
}
});
}
Then, I will be making async query to get the sku details -
private void handleManagerAndUiReady() {
// Start querying for SKUs
List<String> inAppSkus = mBillingProvider.getBillingManager()
.getSkus(SkuType.INAPP);
mBillingProvider.getBillingManager().querySkuDetailsAsync(SkuType.INAPP,
inAppSkus,
new SkuDetailsResponseListener() {
#Override
public void onSkuDetailsResponse(int responseCode,
List<SkuDetails> skuDetailsList) {
if (responseCode == BillingResponse.OK
&& skuDetailsList != null) {
for (SkuDetails details : skuDetailsList) {
Log.w(TAG, "Got a SKU: " + details);
}
}
}
});
// Show the UI
displayAnErrorIfNeeded();
}
Then I got the listener getting an error yet the connection is made without any issues.
D/StoreListFragment: onCreate
I/StoreListFragment: SkuDetailsResponseListener response code: -1
D/StoreListFragment: onViewCreated
I/BillingManager: onBillingSetupFinished() response: 0
So I had to figure out for some time and gave up to check the basics of Play Billing Library -
https://medium.com/exploring-android/exploring-the-play-billing-library-for-android-55321f282929
That is where I found the solution, I just put the query in the connection where it is successfully connected. I realized the play billing library doesn't check the connection BEFORE it goes querying the sku details or am I doing wrong somewhere since the tutorial is working fine?
private void createBillingClient() {
mBillingClient = BillingClient.newBuilder(getActivity()).setListener(this).build();
mBillingClient.startConnection(new BillingClientStateListener() {
#Override
public void onBillingSetupFinished(int billingResponse) {
if (billingResponse == BillingClient.BillingResponse.OK) {
Log.i(TAG, "onBillingSetupFinished() response: " + billingResponse);
//setting up a listener for the queries
SkuDetailsResponseListener responseListener = new SkuDetailsResponseListener() {
#Override
public void onSkuDetailsResponse(int responseCode,
List<SkuDetails> skuDetailsList) {
Log.i(TAG, "response code: " + responseCode);
}
};
List<String> skuList = Arrays.asList("sku_01", "sku_02");
SkuDetailsParams skuDetailsParams = SkuDetailsParams.newBuilder()
.setSkusList(skuList).setType(BillingClient.SkuType.SUBS).build();
mBillingClient.querySkuDetailsAsync(skuDetailsParams, responseListener);
} else {
Log.w(TAG, "onBillingSetupFinished() error code: " + billingResponse);
}
}
#Override
public void onBillingServiceDisconnected() {
Log.w(TAG, "onBillingServiceDisconnected()");
}
});
}
I tried this logic to check if the connection is ready then execute the runnable like as trivial drive - citing this url. It looks like the logic doesn't check if the billing connection is pending....
https://github.com/zumrywahid/in_app_example
Is Billing Client connected? : false
Client is already in the process of connecting to billing service.
onBillingSetupFinished() error code: 5
BillingManager constructor is already starting connection and if you initialize the manager and immediately call any method that passes runnable to executeServiceRequest(Runnable runnable) will also try to start connection at the same time. You can disable startServiceConnection() in the constructor since connection status is always checked in executeServiceRequest() and starts connection if needed.
I solved this issue in the following way:
I created a helper class that manages the billing and added listeners to it, so I can do things only when other actions are ready.
For example, I tried to show a list of in-app purchases in the onCreate of the activity, but the list was always empty. That was because the result of querySkuDetailsAsync was received after the code that displayed the list was executed.
I created a listener and I only display the list after the result is received.
interface BillingResponseIsAvailableListener {
void onBillingResponseIsAvailable(List<SkuDetails> theList);
}
public class BillingTools {
List<String> skuList = new ArrayList<>();
private List<BillingResponseIsAvailableListener> listeners = new ArrayList<>();
private BillingClient billingClient;
private Context context;
public BillingTools(Context context) {
this.context = context;
createSKUList(); //method to create the list of sku's for your app
}
public void addListener(BillingResponseIsAvailableListener toAdd) {
listeners.add(toAdd);
}
and where you query the sku list, just add the listener when the list is ready:
billingClient.querySkuDetailsAsync(params.build(), (billingResult2, skuDetailsList) -> {
if (billingResult2.getResponseCode() == BillingClient.BillingResponseCode.OK && skuDetailsList != null) {
for (BillingResponseIsAvailableListener bL : listeners) {
bL.onBillingResponseIsAvailable(skuDetailsList);
}
}
});
The above code is just an example, I recommend you create your own listeners for whatever you need to do. It is part of a method called queryOptionsList();
In the calling activity, just make the listener do whatever you need to do with the list:
BillingTools takeTheirMoney = new BillingTools(this);
takeTheirMoney.startBillingClient();
takeTheirMoney.addListener(new BillingResponseIsAvailableListener() {
#Override
public void onBillingResponseIsAvailable(List<SkuDetails> theList) {
showPurchaseOptions(findViewById(R.id.purchases_container), theList, takeTheirMoney);
}
});
takeTheirMoney.queryOptionsList();
If you are having problems with the connection not being ready, just add another listener to monitor when the connection is successful, and in that listener place additional actions (which can have their own listeners as well).
Related
I want to find out whether the user active subscription to Basic/Premium content or not from the MainActivity. There is a BillingClientLifecycle class initiating the subscription process. As I understood, queryPurchses should show whether the user has active subscription or not. But apparently it shows (by the Toasts that I put there to show the subscription status) the user is subscribed even when the user is actually not subscribed.
public void queryPurchases() {
if (!billingClient.isReady()) {
Log.e(TAG, "queryPurchases: BillingClient is not ready");
}
Log.d(TAG, "queryPurchases: SUBS");
Purchase.PurchasesResult result = billingClient.queryPurchases(BillingClient.SkuType.SUBS);
if (result == null) {
Log.i(TAG, "queryPurchases: null purchase result");
processPurchases(null);
///
Toast.makeText(applicationContext,"queryPurchases: null purchase result", Toast.LENGTH_SHORT).show();
} else {
if (result.getPurchasesList() == null) {
Log.i(TAG, "queryPurchases: null purchase list");
processPurchases(null);
///
Toast.makeText(applicationContext,"queryPurchases: null purchase list", Toast.LENGTH_SHORT).show();
} else {
processPurchases(result.getPurchasesList());
///
Toast.makeText(applicationContext,"user has subscription!", Toast.LENGTH_SHORT).show();
}
}
}
What am I doing wrong here? I want to update the main activity according to the subscription status. The BillingClientLifecycle is as below:
public class BillingClientLifecycle implements LifecycleObserver, PurchasesUpdatedListener,
BillingClientStateListener, SkuDetailsResponseListener {
private static final String TAG = "BillingLifecycle";
Context applicationContext = MainActivity.getContextOfApplication();
/**
* The purchase event is observable. Only one observer will be notified.
*/
public SingleLiveEvent<List<Purchase>> purchaseUpdateEvent = new SingleLiveEvent<>();
/**
* Purchases are observable. This list will be updated when the Billing Library
* detects new or existing purchases. All observers will be notified.
*/
public MutableLiveData<List<Purchase>> purchases = new MutableLiveData<>();
/**
* SkuDetails for all known SKUs.
*/
public MutableLiveData<Map<String, SkuDetails>> skusWithSkuDetails = new MutableLiveData<>();
private static volatile BillingClientLifecycle INSTANCE;
private Application app;
private BillingClient billingClient;
public BillingClientLifecycle(Application app) {
this.app = app;
}
public static BillingClientLifecycle getInstance(Application app) {
if (INSTANCE == null) {
synchronized (BillingClientLifecycle.class) {
if (INSTANCE == null) {
INSTANCE = new BillingClientLifecycle(app);
}
}
}
return INSTANCE;
}
#OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
public void create() {
Log.d(TAG, "ON_CREATE");
// Create a new BillingClient in onCreate().
// Since the BillingClient can only be used once, we need to create a new instance
// after ending the previous connection to the Google Play Store in onDestroy().
billingClient = BillingClient.newBuilder(app)
.setListener(this)
.enablePendingPurchases() // Not used for subscriptions.
.build();
if (!billingClient.isReady()) {
Log.d(TAG, "BillingClient: Start connection...");
billingClient.startConnection(this);
}
}
#OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
public void destroy() {
Log.d(TAG, "ON_DESTROY");
if (billingClient.isReady()) {
Log.d(TAG, "BillingClient can only be used once -- closing connection");
// BillingClient can only be used once.
// After calling endConnection(), we must create a new BillingClient.
billingClient.endConnection();
}
}
#Override
public void onBillingSetupFinished(BillingResult billingResult) {
int responseCode = billingResult.getResponseCode();
String debugMessage = billingResult.getDebugMessage();
Log.d(TAG, "onBillingSetupFinished: " + responseCode + " " + debugMessage);
if (responseCode == BillingClient.BillingResponseCode.OK) {
// The billing client is ready. You can query purchases here.
querySkuDetails();
queryPurchases();
}
}
#Override
public void onBillingServiceDisconnected() {
Log.d(TAG, "onBillingServiceDisconnected");
// TODO: Try connecting again with exponential backoff.
}
/**
* Receives the result from {#link #querySkuDetails()}}.
* <p>
* Store the SkuDetails and post them in the {#link #skusWithSkuDetails}. This allows other
* parts of the app to use the {#link SkuDetails} to show SKU information and make purchases.
*/
#Override
public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetailsList) {
if (billingResult == null) {
Log.wtf(TAG, "onSkuDetailsResponse: null BillingResult");
return;
}
int responseCode = billingResult.getResponseCode();
String debugMessage = billingResult.getDebugMessage();
switch (responseCode) {
case BillingClient.BillingResponseCode.OK:
Log.i(TAG, "onSkuDetailsResponse: " + responseCode + " " + debugMessage);
if (skuDetailsList == null) {
Log.w(TAG, "onSkuDetailsResponse: null SkuDetails list");
skusWithSkuDetails.postValue(Collections.<String, SkuDetails>emptyMap());
} else {
Map<String, SkuDetails> newSkusDetailList = new HashMap<String, SkuDetails>();
for (SkuDetails skuDetails : skuDetailsList) {
newSkusDetailList.put(skuDetails.getSku(), skuDetails);
}
skusWithSkuDetails.postValue(newSkusDetailList);
Log.i(TAG, "onSkuDetailsResponse: count " + newSkusDetailList.size());
}
break;
case BillingClient.BillingResponseCode.SERVICE_DISCONNECTED:
case BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE:
case BillingClient.BillingResponseCode.BILLING_UNAVAILABLE:
case BillingClient.BillingResponseCode.ITEM_UNAVAILABLE:
case BillingClient.BillingResponseCode.DEVELOPER_ERROR:
case BillingClient.BillingResponseCode.ERROR:
Log.e(TAG, "onSkuDetailsResponse: " + responseCode + " " + debugMessage);
break;
case BillingClient.BillingResponseCode.USER_CANCELED:
Log.i(TAG, "onSkuDetailsResponse: " + responseCode + " " + debugMessage);
break;
// These response codes are not expected.
case BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED:
case BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED:
case BillingClient.BillingResponseCode.ITEM_NOT_OWNED:
default:
Log.wtf(TAG, "onSkuDetailsResponse: " + responseCode + " " + debugMessage);
}
}
/**
* Query Google Play Billing for existing purchases.
* <p>
* New purchases will be provided to the PurchasesUpdatedListener.
* You still need to check the Google Play Billing API to know when purchase tokens are removed.
*/
public void queryPurchases() {
if (!billingClient.isReady()) {
Log.e(TAG, "queryPurchases: BillingClient is not ready");
}
Log.d(TAG, "queryPurchases: SUBS");
Purchase.PurchasesResult result = billingClient.queryPurchases(BillingClient.SkuType.SUBS);
if (result == null) {
Log.i(TAG, "queryPurchases: null purchase result");
processPurchases(null);
///
Toast.makeText(applicationContext,"queryPurchases: null purchase result", Toast.LENGTH_SHORT).show();
} else {
if (result.getPurchasesList() == null) {
Log.i(TAG, "queryPurchases: null purchase list");
processPurchases(null);
///
Toast.makeText(applicationContext,"queryPurchases: null purchase list", Toast.LENGTH_SHORT).show();
} else {
processPurchases(result.getPurchasesList());
///
Toast.makeText(applicationContext,"user has subscription!", Toast.LENGTH_SHORT).show();
}
}
}
/**
* Called by the Billing Library when new purchases are detected.
*/
public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) {
if (billingResult == null) {
Log.wtf(TAG, "onPurchasesUpdated: null BillingResult");
return;
}
int responseCode = billingResult.getResponseCode();
String debugMessage = billingResult.getDebugMessage();
Log.d(TAG, "onPurchasesUpdated: $responseCode $debugMessage");
switch (responseCode) {
case BillingClient.BillingResponseCode.OK:
if (purchases == null) {
Log.d(TAG, "onPurchasesUpdated: null purchase list");
processPurchases(null);
} else {
processPurchases(purchases);
}
break;
case BillingClient.BillingResponseCode.USER_CANCELED:
Log.i(TAG, "onPurchasesUpdated: User canceled the purchase");
break;
case BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED:
Log.i(TAG, "onPurchasesUpdated: The user already owns this item");
break;
case BillingClient.BillingResponseCode.DEVELOPER_ERROR:
Log.e(TAG, "onPurchasesUpdated: Developer error means that Google Play " +
"does not recognize the configuration. If you are just getting started, " +
"make sure you have configured the application correctly in the " +
"Google Play Console. The SKU product ID must match and the APK you " +
"are using must be signed with release keys."
);
break;
}
}
/**
* Send purchase SingleLiveEvent and update purchases LiveData.
* <p>
* The SingleLiveEvent will trigger network call to verify the subscriptions on the sever.
* The LiveData will allow Google Play settings UI to update based on the latest purchase data.
*/
private void processPurchases(List<Purchase> purchasesList) {
if (purchasesList != null) {
Log.d(TAG, "processPurchases: " + purchasesList.size() + " purchase(s)");
} else {
Log.d(TAG, "processPurchases: with no purchases");
}
if (isUnchangedPurchaseList(purchasesList)) {
Log.d(TAG, "processPurchases: Purchase list has not changed");
return;
}
purchaseUpdateEvent.postValue(purchasesList);
purchases.postValue(purchasesList);
if (purchasesList != null) {
logAcknowledgementStatus(purchasesList);
}
}
/**
* Log the number of purchases that are acknowledge and not acknowledged.
* <p>
* https://developer.android.com/google/play/billing/billing_library_releases_notes#2_0_acknowledge
* <p>
* When the purchase is first received, it will not be acknowledge.
* This application sends the purchase token to the server for registration. After the
* purchase token is registered to an account, the Android app acknowledges the purchase token.
* The next time the purchase list is updated, it will contain acknowledged purchases.
*/
private void logAcknowledgementStatus(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);
}
/**
* Check whether the purchases have changed before posting changes.
*/
private boolean isUnchangedPurchaseList(List<Purchase> purchasesList) {
// TODO: Optimize to avoid updates with identical data.
return false;
}
/**
* In order to make purchases, you need the {#link SkuDetails} for the item or subscription.
* This is an asynchronous call that will receive a result in {#link #onSkuDetailsResponse}.
*/
public void querySkuDetails() {
Log.d(TAG, "querySkuDetails");
List<String> skus = new ArrayList<>();
skus.add(Constants.BASIC_SKU);
skus.add(Constants.PREMIUM_SKU);
SkuDetailsParams params = SkuDetailsParams.newBuilder()
.setType(BillingClient.SkuType.SUBS)
.setSkusList(skus)
.build();
Log.i(TAG, "querySkuDetailsAsync");
billingClient.querySkuDetailsAsync(params, this);
}
/**
* Launching the billing flow.
* <p>
* Launching the UI to make a purchase requires a reference to the Activity.
*/
public int launchBillingFlow(Activity activity, BillingFlowParams params) {
String sku = params.getSku();
String oldSku = params.getOldSku();
Log.i(TAG, "launchBillingFlow: sku: " + sku + ", oldSku: " + oldSku);
if (!billingClient.isReady()) {
Log.e(TAG, "launchBillingFlow: BillingClient is not ready");
}
BillingResult billingResult = billingClient.launchBillingFlow(activity, params);
int responseCode = billingResult.getResponseCode();
String debugMessage = billingResult.getDebugMessage();
Log.d(TAG, "launchBillingFlow: BillingResponse " + responseCode + " " + debugMessage);
return responseCode;
}
/**
* Acknowledge a purchase.
* <p>
* https://developer.android.com/google/play/billing/billing_library_releases_notes#2_0_acknowledge
* <p>
* Apps should acknowledge the purchase after confirming that the purchase token
* has been associated with a user. This app only acknowledges purchases after
* successfully receiving the subscription data back from the server.
* <p>
* Developers can choose to acknowledge purchases from a server using the
* Google Play Developer API. The server has direct access to the user database,
* so using the Google Play Developer API for acknowledgement might be more reliable.
* TODO(134506821): Acknowledge purchases on the server.
* <p>
* If the purchase token is not acknowledged within 3 days,
* then Google Play will automatically refund and revoke the purchase.
* This behavior helps ensure that users are not charged for subscriptions unless the
* user has successfully received access to the content.
* This eliminates a category of issues where users complain to developers
* that they paid for something that the app is not giving to them.
*/
public void acknowledgePurchase(String purchaseToken) {
Log.d(TAG, "acknowledgePurchase");
AcknowledgePurchaseParams params = AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchaseToken)
.build();
billingClient.acknowledgePurchase(params, new AcknowledgePurchaseResponseListener() {
#Override
public void onAcknowledgePurchaseResponse(BillingResult billingResult) {
int responseCode = billingResult.getResponseCode();
String debugMessage = billingResult.getDebugMessage();
Log.d(TAG, "acknowledgePurchase: " + responseCode + " " + debugMessage);
}
});
}
}
I am thinking of using shared preferences (instead of the Toasts) inside the BillingClientLifecycle class and retrieve the subscription status from the MainActivity class or any other classes the requires to be notified of the subscription status when the app is launched. Although I prefer not to use the shared preferences and directly call for the subscription info.
The implementation of the billing process looks good, but missing a check to determine whether the subscription is really active at the current moment.
Observing can be done by using LiveData objects. So that we do not need the SharedPreferences or so to hold the state. I'll cover this at the observing part below. A detailed answer:
Purchases list
Let's first explain what the purchases list here exactly means in the billing API:
This is the list of all the purchases the user has for an in-app item or subscription.
These purchases have to be acknowledged by either the app or the backend (recommended via the backend, but both are possible)
This purchases list includes payments which are still pending and also the payments that are not acknowledged yet.
Seeing the acknowledge step being implemented, I assume the acknowledgement of the payment is done successfully.
Point 3 is why it doesn't detect the actual subscribed state, as the state of the purchases aren't checked.
Checking the subscribed state
The queryPurchases() call returns the payments of the user for the requested products. The array that we receive back can have multiple items (mostly one per in-app item or subscription). We need to check them all.
Each purchase has some more data. Here are the methods that we need for checking the state:
getSku() // To verify the product is what we want
getPurchaseState() // To get the actual purchase status
isAcknowledged() // To know if the payment is acknowledged, if not, it means that the payment is not successful yet
In order to check whether a purchase is currently paid and active for the PREMIUM sku:
boolean isPremiumActive = Constants.PREMIUM_SKU.equals(purchase.getSku()) && purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED && purchase.isAcknowledged()
If we want to check if any of the subscriptions is active, we check for the other sku's the same (looping through the sku's and purchases)
* Note that now if isPremiumActive is true, it means that the user currently has an active subscription. This means that if the user canceled his subscription but still has paid till the ending period, this value will still be true. Simply because the user has still the right to access the content until the expiration of the billing period.
* In case the subscription period is really over (cancelled or expired), the billing client will not return the purchase anymore.
Observing the current status
Now that we know how to verify the purchases, we can read this state easily by using LiveData so that we can access it anytime. In the example we already have te LiveData purchases, this one contains all the purchases and is filled after the queryPurchases() call.
Creating the LiveData
Let's create a new LiveData which uses this purchases LiveData, but instead will return true or false based on whether we have the PREMIUM_SKU active:
public LiveData<Boolean> isSubscriptionActive = Transformations.map(purchases, purchases -> {
boolean hasSubscription = false;
for (Purchase purchase : purchases) {
// TODO: Also check for the other SKU's if needed
if (Constants.PREMIUM_SKU.equals(purchase.getSku()) && purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED && purchase.isAcknowledged()) {
// This purchase is purchased and acknowledged, it is currently active!
hasSubscription = true;
}
}
return hasSubscription;
});
Add this block in the BillingClientLifecycle, it will emit the value true or false if the purchases list changes
Observing it
As usual, observe this LiveData in the Activity in which you want to receive the update:
billingClientLifecycle.isSubscriptionActive.observe(this, hasSubscription -> {
if (hasSubscription) {
// User is subscribed!
Toast.makeText(this, "User has subscription!", Toast.LENGTH_SHORT).show();
} else {
// User is a regular user!
}
});
Put this in the MainActivity in your case. It will observe for the subscription changes and trigger in one of the two functions when it changes.
* If the livedata is not wanted but rather a direct way of retrieving the value, you can also just use a boolean field inside the billingClientLifecycle and update this correctly at the processPurchases() method with the same check as above.
Advanced
For a more advanced usage, we can also use the other states of the purchase object:
In case the purchase has a state of Purchase.PurchaseState.PENDING, it means that the Google or the User still have some steps to do to verify the payment. Basically this means that the billing API is not sure whether the payment was fulfilled if this happens. We could inform the user about this state for example too by showing a message to fulfil his payment or so.
If a purchase is paid but not acknowledged yet, it means the acknowledge step in the BillingClientLifecycle was not successful. Additionally, if this is the case, Google Play will automatically refund the payment to the user. For example: for monthly subscriptions the acknowledgement period is 3 days, so after 3 days the user gets the money back and the purchase is removed.
I am using this library for purchases, It may be helpful for you.
https://github.com/anjlab/android-inapp-billing-v3
interface BillingProcessor.IBillingHandler to implement in your main activity
private lateinit var mBillingProcessor: BillingProcessor
val PRODUCT_ID = "remove_ads"//original, set as you want
//val PRODUCT_ID = "android.test.purchased"//testing for purchase
//val PRODUCT_ID = "android.test.canceled"//testing for cancel purchase
in onCreate() method
mBillingProcessor = BillingProcessor(this, "your_license_key", this)
mBillingProcessor.initialize()
//Here after initialization you can check subscription by
if(mBillingProcessor.isSubscribed(PRODUCT_ID)){
//user has Subscribed
}else{
//user has not Subscribed
}
when user click for subscription
mBillingProcessor.subscribe(this, PRODUCT_ID)
implement this method onActivityResult
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (!mBillingProcessor.handleActivityResult(requestCode, resultCode, data)) {
super.onActivityResult(requestCode, resultCode, data)
}
super.onActivityResult(requestCode, resultCode, data)
}
override method of this library
override fun onProductPurchased(productId: String, details: TransactionDetails?) {
if (mBillingProcessor.isPurchased(PRODUCT_ID).toString() == "true") {
//here when user purchased successfully
}
}
this method return you boolean variable
I am implementing IAP in my app. One is for removing ad and other is for adding more puzzle. I was testing that on my device but came across an issue. After buying an item, I am getting response code "Item already owned" but it is not showing in purchase list.
I am setting up my billing client like this,
private void setUpBillingClient(){
mBillingClient = BillingClient.newBuilder(this).setListener(this).build();
mBillingClient.startConnection(new BillingClientStateListener() {
#Override
public void onBillingSetupFinished(#BillingClient.BillingResponse int billingResponseCode) {
if (billingResponseCode == BillingClient.BillingResponse.OK) {
List skuList = new ArrayList<>();
skuList.add(ITEM_SKU_MORE_PUZZLE);
skuList.add(ITEM_SKU_REMOVE_AD);
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP);
mBillingClient.querySkuDetailsAsync(params.build(),
new SkuDetailsResponseListener() {
#Override
public void onSkuDetailsResponse(int responseCode, List skuDetailsList) {
// Process the result.
if (responseCode == BillingClient.BillingResponse.OK
&& skuDetailsList != null) {
for (Object skuDetailsObject : skuDetailsList) {
SkuDetails skuDetails = (SkuDetails) skuDetailsObject;
String sku = skuDetails.getSku();
String price = skuDetails.getPrice();
if (ITEM_SKU_MORE_PUZZLE.equals(sku)) {
btnMorePuzzle.setText(price);
}
else if(ITEM_SKU_REMOVE_AD.equals(sku)) {
btnRemoveAd.setText(price);
}
}
}
}
});
}
}
#Override
public void onBillingServiceDisconnected() {
//Toast.makeText(getApplicationContext(), getResources().getString(R.string.billing_connection_failure), Toast.LENGTH_SHORT);
}
});
queryPurchases();
queryPrefPurchases();
}
First question, why is Billing response is OK here when I have already purchased the item. I don't want to set text of button as price, which is getting set from this response after product is bought.
This is my Onpurchase implementation,
#Override
public void onPurchasesUpdated(int responseCode, #Nullable List<Purchase> purchases) {
if (responseCode == BillingClient.BillingResponse.OK && purchases != null) {
for (Purchase purchase : purchases) {
if (purchase.getSku().equals(ITEM_SKU_REMOVE_AD)) {
mSharedPreferences.edit().putBoolean("ad_free", true).commit();
btnRemoveAd.setText("Done");
btnRemoveAd.setEnabled(false);
}
else if(purchase.getSku().equals(ITEM_SKU_MORE_PUZZLE)){
mSharedPreferences.edit().putBoolean("more_puzzle", true).commit();
btnMorePuzzle.setText("Done");
btnMorePuzzle.setEnabled(false);
}
}
} else if (responseCode == BillingClient.BillingResponse.ITEM_ALREADY_OWNED ) {
// I am getting response "Item already owned" here for item bought but purchase list here is empty
// so i can't do anything for purchased item
}
}
Second question, here I am getting response that my item is already bought but still list is empty.
How to implement it properly?
If someone already bought a product then button should be disabled.
Another doubt is while testing do I have to wait for 1-2 hrs to get that item refunded from playstore to test again or is there any other method.
I am following this code for in-app implementation.
https://github.com/patpatchpatrick/Streakr/
There is only one thing to point out here, that you are querying only the SKU type INAPP items only not the SUBS. I think the type of product you are providing comes under subscriptions not under in-app products which are used to be consumed. That is why your query is empty.
I found a mistake in your onPurchasesUpdated, conditions are set wrongly.
It should go something like this,
if(BillingResponse.OK && purchases != null) {
// update records
} else if(BillingResponse.ITEM_NOT_OWNED){ //this condition was missing
// update records if required or ask to buy
} else if(BillingResponse.ITEM_ALREADY_OWNED ){ // update records}
and i also suggest to update db for this.
I am upgrading the in-app billing in my app from Version 3 to newer code. Version 3 is working fine and it production in my app right now but I've read it will be deprecated eventually.
When I try to test the purchase flow using a static product id (android.test.purchased), the BillingResult result code only returns -1 with a debug message of "Service connection is disconnected". AFAIK, there is no service connection in the newer library but there was in Version 3.
If I use a real in-app product code it tells me I've already purchased it, which is correct but I need to test the actual purchase flow.
I am testing this on an actual device (Pixel 3 XL), not the emulator. I've tried testing it on a separate device that is logged in with a test account (not developer) but I get the same results.
UPDATE: I setup a real (test) in-app managed product in the Developer Console, then installed my app on a device that is logged in with a test (non-developer) account and I'm still getting the "Service connection is disconnected" error. I feel it's something outside of the code but not sure what.
UPDATE 2: I created a new project with nothing in it except the billing code and it worked so there's something in my app that is causing it to break.
UPDATE 3: I created a new project and imported the code from the broken app into it and still getting the same error message. I feel, now, there's something with my package name and Google's servers that's returning the error message.
UPDATE 4: I created a blank project but gave it the same package name as my broken app and the billing worked, so it's not the package name. Now my guess is there's something from the old AIDL billing code (Version 3) that is interfering.
SOLUTION!!! in the application node in the AndroidManifest.xml I had this: tools:node="replace". Removed that attribute and billing now works.
mBillingClient = BillingClient.newBuilder(this).enablePendingPurchases().setListener(this).build();
mBillingClient.startConnection(new BillingClientStateListener() {
#Override
public void onBillingSetupFinished(BillingResult billingResult) {
if (billingResult.getResponseCode() == OK) {
final Purchase.PurchasesResult purchasesResult = mBillingClient.queryPurchases(BillingClient.SkuType.INAPP);
if (purchasesResult.getResponseCode() == OK) {
final List<Purchase> purchases = purchasesResult.getPurchasesList();
for (final Purchase purchase : purchases) {
}
}
}
}
#Override
public void onBillingServiceDisconnected() {
CommonUtils.showToast(mActivity, "disconnected");
}
});
mUnlockPremiumButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
final List<String> skuList = new ArrayList<> ();
skuList.add(getString(R.string.inapp_premium_product_id));
final SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder()
.setSkusList(skuList)
.setType(BillingClient.SkuType.INAPP);
mBillingClient.querySkuDetailsAsync(params.build(),
new SkuDetailsResponseListener() {
#Override
public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetailsList) {
for (SkuDetails skuDetails : skuDetailsList) {
if (getString(R.string.inapp_premium_product_id).equals(skuDetails.getSku())) {
final BillingFlowParams flowParams = BillingFlowParams.newBuilder()
.setSkuDetails(skuDetails)
.build();
final BillingResult result = mBillingClient.launchBillingFlow(mActivity, flowParams);
if (result.getResponseCode() == ITEM_ALREADY_OWNED)
{
CommonUtils.showToast(mActivity, getString(R.string.alert_purchased));
}
else if (result.getResponseCode() != OK)
{
//always returns a getResponseCode of -1 (service disconnected)
}
}
}
}
});
}
});
As the error clearly indicates, the Billing Client is disconnected due to the following possible reasons.
You started the connection but the connection has not finished the setup yet.
Possible race condition since startConnection is an asynchronous process. You can use
billingClient.isReady() to check if it is available.
Your app may have lost the connection/internet after starting it.
Your device/emulator does not support Google Play Services.
The following is a working code in Billing API 3.0 with Android Version Targeted SDK 28.
As documented in Billing API Error Handling section (https://developer.android.com/google/play/billing/integrate) if the connection is lost, app must be responsible to reestablish the connection. I would recommend to use a Singleton class that holds the Billing API and use billingClient.isReady() method to check if the connection is successful, if not attempt to reestablish it. Provided you add logging in the BillingClientStateListener class override methods. I am skipping that code because it is straight forward and well documented in the link I provided above.
public class ApplicationBillingClient
{
static ApplicationBillingClient applicationBillingClient= null;
private static BillingClient billingClient;
private ApplicationBillingClient() {}
private static boolean isInitialized()
{
return applicationBillingClient != null && billingClient != null;
}
private static void initialize(Context applicationContext)
{
try
{
if(applicationContext != null)
{
applicationBillingClient = new ApplicationBillingClient();
BillingClient.Builder builder= BillingClient.newBuilder(applicationContext);
builder.setListener(new PurchaseActivityListener());
builder.enablePendingPurchases();
billingClient = builder.build();
}
LogUtil.info("Initializing the Billing Client");
}
catch (Exception ex)
{
LogUtil.error("Error while initializing billing client", ex);
}
}
public static ApplicationBillingClient getInstance(Context applicationContext)
{
if(isInitialized() == false)
{
initialize(applicationContext);
}
return applicationBillingClient;
}
public void startConnection()
{
billingClient.startConnection(new StateListener());
}
public boolean isReady()
{
return billingClient.isReady();
}
public void getMonthlySubscription()
{
try
{
if(billingClient.isReady())
{
SkuDetailsParams.Builder skuBuilder = SkuDetailsParams.newBuilder();
skuBuilder.setType(BillingClient.SkuType.SUBS);
skuBuilder.setSkusList(Arrays.asList(new String[]{MONTHLY_BILLING_SUBSCRIPTION_SKU}));
SkuDetailsParams params = skuBuilder.build();
billingClient.querySkuDetailsAsync(params, new SkuDetailsListener());
}
}
catch (Exception ex)
{
LogUtil.error("Error while querying async SKU for Monthly Subscription", ex);
}
}
}
//In your activity
ApplicationBillingClient appBillingClient =
ApplicationBillingClient.getInstance(applicationContext);
if (appBillingClient.isReady() == false)
{
appBillingClient.startConnection();
}
else
{
appBillingClient.getMonthlySubscription();
}
This was the application node, in the AndroidManifest, when the billing was breaking:
<application
android:allowBackup="true"
android:icon="#mipmap/ic_launcher"
android:label="#string/app_name"
android:supportsRtl="true"
android:theme="#style/AppTheme"
tools:node="replace">
...
</application>
removing tools:node="replace" fixed the billing. Hopefully this will save someone the days I wasted.
I found this when trying to understand RxJava in this example
getUserObservable method which emit some users i need to know why he put
.subscribeOn(Schedulers.io()) while he already call it on main funcion
i provided snipt for both methods
i know that subscribeOn will make the process happened on background thread , but when he called it two times is this will made any different i don't know , as i understand just calling it one time in getUsersObservable will be enough
private Observable<User> getUsersObservable() {
String[] maleUsers = new String[]{"Mark", "John", "Trump", "Obama"};
final List<User> users = new ArrayList<>();
for (String name : maleUsers) {
User user = new User();
user.setName(name);
user.setGender("male");
users.add(user);
}
return Observable
.create(new ObservableOnSubscribe<User>() {
#Override
public void subscribe(ObservableEmitter<User> emitter) throws Exception {
for (User user : users) {
if (!emitter.isDisposed()) {
emitter.onNext(user);
}
}
if (!emitter.isDisposed()) {
emitter.onComplete();
}
}
}).subscribeOn(Schedulers.io());
}
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_map_operator);
getUsersObservable()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.map(new Function<User, User>() {
#Override
public User apply(User user) throws Exception {
// modifying user object by adding email address
// turning user name to uppercase
user.setEmail(String.format("%s#rxjava.wtf", user.getName()));
user.setName(user.getName().toUpperCase());
return user;
}
})
.subscribe(new Observer<User>() {
#Override
public void onSubscribe(Disposable d) {
disposable = d;
}
#Override
public void onNext(User user) {
Log.e(TAG, "onNext: " + user.getName() + ", " + user.getGender() + ", " + user.getAddress().getAddress());
}
#Override
public void onError(Throwable e) {
Log.e(TAG, "onError: " + e.getMessage());
}
#Override
public void onComplete() {
Log.e(TAG, "All users emitted!");
}
});
}
This is normally done for 2 reasons:
You see at the place, where the method is invoked, on which scheduler the observable is is subscribed on (if this is done within the method you do not know from outside).
You have the possibility to use the same method and subscribe to it on different schedulers in different places of your app.
But if you know for sure, it's always going to be the same scheduler, you can as well move the subscribeOn() into the method itself.
EDIT
I didn't see, that .subscribeOn(Schedulers.io()) is already called inside the getUsersObservable() method. It does not make sense to call it inside the method and outside, when calling the method. That seems like a bug to me. As described, above, usually .subscribeOn() is called outside the method, but you can also do it inside. Doing both makes no sense.
I have implemented in-app purchased in my application and my product type is Managed and i am using API version 3.
When i make purchase from my credit card it is successfully done.
But the problem is if i uninstall my application and want to purchase this with same account it will charge me again?
According to Google rules of managed product type we only purchase the product once? But why is this happening ?
any one help me please?
here is my PurchaseActivity.java class
public abstract class PurchaseActivity extends BlundellActivity implements OnIabSetupFinishedListener, OnIabPurchaseFinishedListener {
private IabHelper billingHelper;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_purchase);
setResult(RESULT_CANCELED);
billingHelper = new IabHelper(this, AppProperties.BASE_64_KEY);
billingHelper.startSetup(this);
}
#Override
public void onIabSetupFinished(IabResult result) {
if (result.isSuccess()) {
Log.d("In-app Billing set up" + result);
dealWithIabSetupSuccess();
} else {
Log.d("Problem setting up In-app Billing: " + result);
dealWithIabSetupFailure();
}
}
protected abstract void dealWithIabSetupSuccess();
protected abstract void dealWithIabSetupFailure();
protected void purchaseItem(String sku) {
billingHelper.launchPurchaseFlow(this, sku, 123, this);
}
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
billingHelper.handleActivityResult(requestCode, resultCode, data);
}
*/
#Override
public void onIabPurchaseFinished(IabResult result, Purchase info) {
if (result.isFailure()) {
dealWithPurchaseFailed(result);
} else if (pmg.SKU.equals(info.getSku())) {
dealWithPurchaseSuccess(result, info);
}
finish();
}
protected void dealWithPurchaseFailed(IabResult result) {
Log.d("Error purchasing: " + result);
}
protected void dealWithPurchaseSuccess(IabResult result, Purchase info) {
Log.d("Item purchased: " + result);
// DEBUG XXX
// We consume the item straight away so we can test multiple purchases
billingHelper.consumeAsync(info, null);
// END DEBUG
}
#Override
protected void onDestroy() {
disposeBillingHelper();
super.onDestroy();
}
private void disposeBillingHelper() {
if (billingHelper != null) {
billingHelper.dispose();
}
billingHelper = null;
}
}
This is working as intended - in your code you are consuming the in-app purchase immediately, which means you can then purchase it again:
protected void dealWithPurchaseSuccess(IabResult result, Purchase info) {
Log.d("Item purchased: " + result);
// DEBUG XXX
// We consume the item straight away so we can test multiple purchases
billingHelper.consumeAsync(info, null);
// END DEBUG
}
There's nothing that says you can't purchase a managed product more than once. What you can't do is purchase a managed product before a previous purchase of the same managed item has been consumed. So this is working exactly as intended, and if you remove that call to consumeAsync, you'll see that you can't purchase it again.
Sample use case:
Imagine some game where you can purchase extra lives. First, the user would purchase the extra lives (a managed in app product), your game (client or server) would then add those lives to the user's profile, for example, and assuming that was successful, you'd tell Google Play that the purchase has been consumed.
This is important in order to handle error cases - for example say the user's device dies in between the initial purchase and the addition of lives to the user's profile. Your app can then, the next time it's launched, try again to add those lives, and consume the purchase on success. And, obviously you wouldn't want the user trying to purchase even more lives before you successfully grant them - which is why you can't purchase a managed product twice before it's been consumed.