I'm implementing Android in app purchases, with BillingClient api.
I successfully set up billing client, like this:
billingClient = BillingClient.newBuilder(this)
.enablePendingPurchases()
.setListener(this).build()
And I implement the code I want to make the in app purchase, which in my case is a subscription.
My issue is as follows, I need to check (periodicly) if the user still has an active subscription, or if they canceled it. From what I read, I can do a call of the method, queryPurchases(PRODUCT_ID). I am doing this as follows:
override fun onBillingSetupFinished(billingResult: BillingResult?) {
if (billingResult != null) {
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK){
// The BillingClient is ready. You can query purchases here.
Log.d(TAG_PURCHASES_ACTIVITY, "BillingClient ready and connected")
billingClient.queryPurchases(MONTHLY_AGENT_SUBSCRIPTION_PRODUCT_ID)
}
else{
Log.d(TAG_PURCHASES_ACTIVITY, "BillingClient ERROR: ${billingResult.responseCode}")
}
}
}
However, I get this message in the console:
W/BillingClient: getPurchase() failed. Response code: 3
Now after researching, many have said that this is coming from old phones, with older versions of google play services. But I am using new phone (Pixel 3a), which is up to date. This phone also has the google account registered in the google play console for testing.
Why I am getting this message?
Also, is this really the correct way to check if the subscription is still active?
I've tried for ages to find this solution, thanks for any help!
I was making a mistake, passing the product id as a parameter to the query request.
This is what I needed to do:
var list = billingClient.queryPurchases(SkuType.SUBS)
Related
Some of my users tell me that my app forgets the purchased subscriptions every now and then. It works for 3-4 days and then it forgets them. This is a very important issue as users might suspect fraud. I am using billing library 4.0.0 and I have implemented the billing logic as per Google's guidelines.
From what I have gathered it happens when for some reason the billing service connection is interrupted. (Play Store is updating for example)
I have managed to replicate this scenario the following way
- Disable internet connection
- Clearing Play Store app data
- Fresh launch of my app.
- Call billingClient.startConnection()
onBillingSetupFinished called with responseCode BILLING_UNAVAILABLE
user sees -> The app says "no subscription purchased"
- Enable internet connection
- re-initialize BillingClient.
onBillingSetupFinished called with responseCode OK. billingClient.isReady() returns true.
- Call billingClient.queryPurchasesAsync() and billingClient.querySkuDetailsAsync().
onSkuDetailsResponse is called with the skuDetailsList filled with all the proper data. However:
onQueryPurchasesResponse is called with empty purchase list -> Again user sees "no subscriptions purchased"
Important If at this point I open Play Store it shows the purchased subscriptions. But the app still gets an empty purchases list.
If I keep calling billingClient.startConnection() or billingClient.queryPurchasesAsync() at some point after about 10 minutes one attempt will succeed and return a non empty purchases list.
Is it possible to tell Play Store to refresh its subscription data for my app? How can someone handle this scenario gracefully ?
You need to call acknowledgePurchase for every purchase.
See the official docs and the article for more details.
Worked for me!
BillingClient billingClient = ...;
billingClient must be ready -> billingClient.isReady() == true;
So, always call billingClient.queryProductDetailsAsync(...) from
#Override
public void onBillingSetupFinished(BillingResult billingResult) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
// The BillingClient is ready. You can query purchases here.
queryProductDetails();
queryPurchases();
}
}
when initialize the billingClient connection! Keep it up :)
I created an Android app that supports in-app-purchase.
In order to develop in app purchase function, I need to setup in-app-purchase in GooglePlay console.
In order to create in-app-purchase in GooglePlay console, I need to upload app first. However, my app needs in-app-purchase to complete the development.
Therefore, I uploaded my incomplete app to GooglePlay in closed Alpha track. From some research, I learnt I also need to publish the app as otherwise in app purchase won't work.
However, the situation is Google takes long time to review the app and the status of the app is stuck in "being reviewed" for over 3 days. I don't know if the review will be passed as the app is incomplete and it is not functional.
OK, after some research, I learnt that I can use test in-app-purchase item such as
android.test.purchase and android.test.canceled
Therefore, I started testing my in-app-purchase function where my mobile app calls my backend to validate the purchase result and then I start receiving 400 error from Google on my server side.
At the following is my server side code that validates the purchase that received from mobile app:
let performGooglePlayValidationLogic = async function(userid, sku, purchaseToken) {
let auth = new google.auth.GoogleAuth({
credentials: {
client_email: '4655XXXXXXXX-compute#developer.gserviceaccount.com',
private_key: '-----BEGIN PRIVATE KEY-----XXXXXXXXXXXXX-----END PRIVATE KEY-----\n'
},
scopes: ['https://www.googleapis.com/auth/androidpublisher'],
});
const authClient = await auth.getClient();
google.options({ auth: authClient });
let purchaseResponse = {};
let purchases = google.androidpublisher({version: 'v3',}).purchases;
try {
//products
purchaseResponse = await purchases.products.get({
packageName: 'com.mycompany.myapp',
productId: sku,
token: purchaseToken,
});
if (purchaseResponse.data.purchaseState !== 0) {
throw new BadRequestException('Purchase is either Pending or Cancelled!');
} else if (purchaseResponse.data.consumptionState !== 0) {
throw new BadRequestException('Purchase is already consumed!');
} else {
At the following is the parameter that I passed into this method (The value are received from mobile app)
sku: android.test.purchased
purchaseToken: inapp:com.mycompany.myapp:android.test.purchased
Then I received exception at line purchaseResponse = await purchases.products.get({:
status:400
statusText:'Bad Request'
url: https://androidpublisher.googleapis.com/androidpublisher/v3/applications/com.mycompany.myapp/purchases/products/android.test.purchased/tokens/inapp%3Acom.mycompany.myapp%3Aandroid.test.purchased'
I am not sure if this 400 error is caused by my app is still under review so package name com.mycompany.myapp might yet be available, or it is because I did something wrong?
From my research it looks like 400 error is most likely cause by an invalid purchase token, which in my case it is inapp:com.mycompany.myapp:android.test.purchased I do feel it looks suspicious but it is received from my mobile app where is issued by GooglePlay.
ok after 2 day's investigation, it has been discovered that:
The purchasetoken (inapp:com.mycompany.myapp:android.test.purchased) generated by inapppurchase item android.test.purchased cannot be used on server side validation as androidpublisher.googleapis.com will return error 400 for such purchasetoken.
I hope my 2 day's work can help others with saving their time.
I have updated my application to use v2.1.0 (from an earlier 2.0.1 release) of the android play billing library and I am no longer receiving purchase updates on completion of a pending purchase.
I construct my billing client setting the listener to the current class, that implements PurchasesUpdatedListener:
billingClient = BillingClient.newBuilder( getActivity() )
.setListener( this )
.enablePendingPurchases()
.build();
I then launch a purchase and use the "slow test card approves after a few minutes"
BillingFlowParams.Builder purchaseParamsBuilder = BillingFlowParams.newBuilder()
.setSkuDetails( product );
billingClient.launchBillingFlow( getActivity(), purchaseParamsBuilder.build() );
My listener gets called after this indicating the purchase is pending:
#Override
public void onPurchasesUpdated( BillingResult billingResult, List<Purchase> purchases )
{
int responseCode = billingResult.getResponseCode();
if (responseCode == BillingClient.BillingResponseCode.OK)
{
// I get to here with a Purchase.PurchaseState.PENDING
}
}
However this listener does NOT get called when the purchase completes. I leave the application open and I see the notification from the play store saying the purchase was successful in the notification bar.
If I query the purchases manually after this notification I can get the updated purchase however this doesn't seem like a valid approach for handling the purchase.
Does anyone know what I am doing wrong or has this process changed?
This was an issue in the Play Store app (v17.9.17). The code here is correct and there is no error in the usage of the Play Billing library in the above, i.e. onPurchasesUpdated should be called when the pending transaction completes (either successfully or cancelled).
Google has isolated the issue and it will be fixed in the next update.
Reference for the bug report can be found here:
https://issuetracker.google.com/issues/146480197
I have a one time purchase in my application. It enables all the features of the application.
When a user purchases the sku on their phone... everything works fine. We see the event returned by the billing client and record the purchase of the premium mode via setting a boolean in a local db. The local Google Play cache is updated properly.
Our problem is when they install on another device. Imagine wanting to use it on a tablet and your phone OR you lost your phone and got another.
We have been using the "query purchases" method of the billing client to validate a purchase. They install the app and I would assume that the purchase history would be added to the local cache at this time.
Generally, it works. But a non-negligible amount of users need to restart their phones several times or wait DAYS from Google to get their ##$% together and actually add the purchase history to their cache.
Their docs say that method only queries the local cache. https://developer.android.com/reference/com/android/billingclient/api/BillingClient#querypurchases
The Google Play Developer API lets use query for purchases but we need the purchase token in order to validate anything. If Google Play can't return a purchase token since it has no record of a purchase... what do we do?
How does everyone else deal with this problem?
queryPurchases relies on the cache on device. So if a user's device doesn't have cache updated in time, you will see this behavior. To solve it you should use queryPurchaseHistoryAsync inside queryPurchases as follows
override fun onBillingSetupFinished(billingResult: BillingResult) {
when (billingResult.responseCode) {
BillingClient.BillingResponseCode.OK -> {
...
queryPurchasesAsync()
}
...
}
}
fun queryPurchasesAsync() {
val purchasesResult = HashSet<Purchase>()
var result = playStoreBillingClient.queryPurchases(BillingClient.SkuType.INAPP)
if(nullOrEmpty(result)){
queryPurchaseHistoryAsync(...) // response is returned to onPurchaseHistoryResponse
}else{
... // here you add your existing logic to give users access to products and services
}
}
Aside: since you use the tag google-play-developer-api, does your app have a server? If so you could set the boolean on your server instead of using a local cache and grab the entitlement information from your server. The advantage of this approach is that when your app becomes cross-platform then it doesn't matter if the user made a purchase through Android or the web, they will get access to the premium content by checking with your server.
ref: https://github.com/googlesamples/android-play-billing/tree/master/TrivialDriveKotlin
I implemented in-app billing into my app and am now testing its handling of refunds.
I bought my app's managed in-app billing item with a test account and refunded it. My app got the refund broadcast as expected and it sees that the item was refunded when restoring transactions, so everything is good up to that point.
My problem is that I can't re-buy the item to test other scenarios.
When I try to purchase the item, the Google Play interface comes up and displays an error message saying "You already own this item." with 2 buttons "OK" and "Details".
If I press details, Google Play crashes and I return to my app.
Did anyone have a similar experience?
Is it forbidden for a user to purchase an in-app item if they previously had it refunded?
I was seeing the same issue. GP crash and everything.
In addition to waiting a few hours, you may want to open up 'Google Play' app info and clear cache and clear data. This solved it for me. It appears GP caches purchase information on the device and only checks Google's servers rarely, if ever, for refund information.
Update:
You may also want to kill the Google Play process since it appears to keep purchase info in memory too.
I asked Google about this issue and they told me that it's not possible to re-buy an in-app billing item on Google Play if it was previously refunded.
But when I tried to buy it again about 24 hours later, the purchase went through ...
So it looks like it's possible to re-buy, but only after some delay.
I know this is an old question, but i have been looking for an answer to this same question and eventually came to my own conclusion. Google doesn't spell it out, but I believe they want you to decide on your own logic as to how to handle cancelled and refunded purchases. Another point to keep in mind is that there there is essentially no difference between a consumable and non consumable managed product. All managed products are consumable.
For me, when a user cancels a purchase, or if I decide to give the user a refund, what I want to happen is that 1) the user receives their money back and 2) the user loses access to the paid feature and 3) the user has the option to purchase the feature again if they choose.
What I did was to check the purchaseState of the purchase on my back end server using the in-app billing API. If the returned purchaseState is a 1 (canceled) or 2 (refunded), I consume the purchase in my app. Google handles item 1, giving the user their money back. The logic in my app handles 2, locking access to the paid features. Consuming the purchase handles 3, giving the user the option to purchase the feature again.
The basic gist of it is, when a purchase is sent to my back end server for verification, I check the purchase state. If the purchase state is a 1 or a 2, I return an appropriate code to my app. When my app receives the code indicating the purchase is cancelled or refunded, my app consumes the purchase.
I use the PHP version of the API, so my simplified code to get the purchase state is :
$purchases = $service->purchases_products->get($packageName, $productId, $purchaseToken);
$purchaseState = $purchases->getPurchaseState();
if($purchaseState === 1){
$serverResponseCode = 3;
}
if($purchaseState === 2){
$serverResponseCode = 4;
}
...and then in my app, I check the server response codes.
if(serverResponseCode == 3 || serverResponseCode ==4 ){
lockFeatures();
ConsumeParams params = ConsumeParams.newBuilder().setPurchaseToken(purchase.getPurchaseToken()).build();
billingClient.consumeAsync(params, listener);
}
I hope this helps someone else looking for an answer to this problem.
In case somebody needs android and not kotlin code. All the explanation that smitty wrote:
When starting up the application , You have to check queryPurchases and look for the refunded items.
like that:
if (purchase.getPurchaseState() != Purchase.PurchaseState.UNSPECIFIED_STATE)
{
handleConsumablePurchasesAsync(purchasesList);
return false;
}
Than you CONSUME this item.
smitty1 is a Genius
private void handleConsumablePurchasesAsync(List<Purchase> consumables) {
Log.d(TAG, "handleConsumablePurchasesAsync");
for (Purchase purchase : consumables) {
ConsumeParams params = ConsumeParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();
billingClient.consumeAsync(params, (billingResult, purchaseToken) -> {
if (billingResult.getResponseCode() == OK) {
Log.d(TAG, "Consumed the old purchase that hasn't already been acknowledged");
} else {
Log.d(TAG, "Error consume the old purchase that hasn't already been acknowledged -> %s" + String.valueOf(billingResult.getResponseCode()));
}
});
}
}
I noticed that by checking the Remove Entitlements field on the refund page, you will be able to re-buy the product outright without waiting as suggested by the accepted answer.