I've been managing Google Play subscriptions from my Android app for a long time, using Google Play Billing Library version 4.0 since last year (it's not feasible to upgrade to 5.0 at this moment). Now I want to offer free trial periods in subscriptions, so I have created one offer in the Google Play console for each of my subscriptions, with the following configuration:
Offer's eligibility criteria: Developer determined (I don't want Google to decide which users may enjoy the offers, I want to be the one to decide it from my code, applying my own criteria)
A phase of "Free trial" type, 1 month duration.
When I need, for example, to get the details of a "my_monthly_plan" subscription from my app, my code is as follows:
val params = SkuDetailsParams.newBuilder().apply {
setSkusList(listOf("my_monthly_plan"))
setType(SkuType.SUBS)
}
myBillingClient.querySkuDetailsAsync(params.build()) { billingResult, skuDetailsList ->
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
if (!skuDetailsList.isNullOrEmpty()) {
// A not empty products details list has been successfully retrieved.
...
} else {
// Products details list is null or empty.
...
}
} else {
// The response code is not OK.
...
}
}
}
If everything is ok, I receive a SkuDetails object containing something like this:
SkuDetails: {"productId":"my_monthly_plan", ... ,"price":"49,00 €", ... ,"freeTrialPeriod":"P1M"}
First problem: I always receive here an empty freeTrialPeriod object. All subscriptions and offers are correctly created on Google Play. Maybe I just have to wait a few more hours or days for them to be visible from my app?
When users choose which subscription (defined by skuDetails) they want to buy from my app, the following code is executed:
val flowParams = BillingFlowParams.newBuilder()
.setSkuDetails(skuDetails)
.build()
billingClient.launchBillingFlow(activity, flowParams)
Second problem: at this point I have no idea how to tell Google if the free trial should be applied to the purchase or not.
Let me explain with a couple of example scenarios:
Scenario 1
I have a customer A who has never bought subscriptions in my app, and I want him to enjoy the free trial when he buys for the first time.
The querySkuDetailsAsync call returns a plan with these SkuDetails: {"productId":"my_monthly_plan", ... ,"price":"€49.00", ... ,"SkuDetailsParams":"P1M"} (I am optimistic and I hope that my first problem will be solved and I get to receive a non-empty value in SkuDetailsParams)
When customer A purchases the subscription, I use the SkuDetails obtained in the previous step to parameterize the launchBillingFlow call and launch the purchase process.
I assume that Google is going to apply the free trial to my customer A for one month, and if the customer auto-renews the subscription after this free month will automatically be charged the price without offer, €49 each month. But, how can I be sure that the customer is not going to be charged €49 from this first purchase? Do I need to specify it somehow when calling to launchBillingFlow? Or maybe there is another API call for this?
Scenario 2
I have another customer B who has never bought subscriptions in my app either, but for whatever reason I don't want him to enjoy the free trial when he buys for the first time (that's why I chose the "Developer determined" Offer's eligibility criteria).
The querySkuDetailsAsync call returns the same SkuDetails as before.
When customer B purchases the subscription, should I use the same SkuDetails to parameterize the launchBillingFlow call to launch the purchase process? How do I tell Google that the free trial does not apply to this customer B, that he has to be charged €49 from this first purchase?
Any idea to solve my two problems will be very welcome. Thanks in advance.
Related
I'm using InAppbilling v5 in Android to retrieve the price of the Items, everything is working correctly, however when "Google" review my app they rejected because I'm not following the Subscription guidelines. In my test the price is retrived correctly but for in the Screenshots of Google there is a "Free", formatted price string instead $9.00 so I'm not sure how to fix this issue because apparently only happens when Google review my app. I'm using the following code to retrieve the products
billingClient.queryProductDetails(
subscriptions.toQueryProduct(ProductType.SUBS)
).productDetailsList
Then I'm retrieving the offer details like this:
val price = productDetails.subscriptionOfferDetails
?.firstOrNull()
?.pricingPhases?.pricingPhaseList
?.firstOrNull()
?.formattedPrice ?: ""
In my devices and emulator works correctly displaying the correct price, however for google it display "free" as formattedPrice How I can solve this?
In my Subscriptions in the Play Store console I have only one price/base offer with a 7 days free trial.
I found the issue, the problem was that in the new version v5 if the user never had any subscription there will be 2 phases, the "free" trial which price is free and the subscription price after trial, I tested with my user and since I already had any subscription the "free" was not available, but for new users that never had any subscription there was a free opion. for now in my code I'm filtering the free with this code
// there can be several pricingPhases and trial can be "free"
val formattedPrice = offerDetails?.pricingPhases
?.pricingPhaseList
?.firstOrNull { it.priceAmountMicros > 0 }
?.formattedPrice
using it.priceAmountMicros > 0 will filter any item that is not "free" or has a price
I have 4 subscription types in the app I'm developing. Two monthly (one with discount) and two yearly (one with discount). I am in the testing step. When I ask the purchases history I am not getting all the lasts subscriptions of each type as the documentation says. And also the purchases I get are not the lasts of each SKU type. Did anyone have this issue?
It's giving me problems with grace period as I don't get the last purchase I did so I don't get the updated expiry time.
I checked Google Play Console order management and I see the purchases, and also in my Google Play app, so I don't know what's the problem?
It's difficult to understand your situation without you providing a code snippet. But here is a general implementation of queryPurchaseHistoryAsync. If you are still having problems, please provide more context such as your actual code snippet and where you are making the call. For the following snippet -- just for testing -- I am making the call right before I call queryPurchases.
private fun queryPurchaseHistoryAsync(){
playStoreBillingClient.queryPurchaseHistoryAsync(BillingClient.SkuType.SUBS){
responseCode, purchasesList ->
if(purchasesList.isNullOrEmpty()){
Log.d(LOG_TAG,"history for SUBS is empty")
}else{
Log.d(LOG_TAG,"history subs has ${purchasesList.size} items : ${purchasesList.toString()}")
}
}
playStoreBillingClient.queryPurchaseHistoryAsync(BillingClient.SkuType.INAPP){
responseCode, purchasesList ->
if(purchasesList.isNullOrEmpty()){
Log.d(LOG_TAG,"history for INAPP is empty")
}else{
Log.d(LOG_TAG,"history INAPP has ${purchasesList.size} items : ${purchasesList.toString()}")
}
}
}
Also in my cases, I have no problem getting the purchase histories.
Until june 20th 2016 i was able to cancel test purchases done in my app.
Doing multiple in-app purchases (not consumable) from the same test account made it easy to develop and test the code without too much hazzle.
After 20th june 2016, the purchases did not show in my merchant account and
i was unable to do more than 1 purchase from my test account. All i got was the: "you already own this item" message.
I logged a request to the google developer support group and the answer was:
Beginning June 20, 2016, we changed test purchases for one-time in-app purchases (IAPs).
Previously, test purchases for one-time IAPs generated order IDs. Starting June 20, 2016, one-time IAPs do not generate official order IDs (if at all) and will not appear in the Merchant Center. This behavior already applies to subscription IAPs.
You can learn more about testing in-app billing in the Android Developers Help Center: https://developer.android.com/google/play/billing/billing_testing.html#testing-purchases
allright.. so i go to the mentioned link and theres a section there:
Canceling completed test purchases
which states:
Google Play accumulates completed test purchases for each user but does not pass them on to financial processing.
In some cases, you might want to manually cancel a test purchase to continue testing. To do so, open the app page in the Play Store. If the test purchase that you want to cancel is a subscription, you can also use the cancel() method of the Purchases.subscriptions API.
Important: The refund() and revoke() methods of the Purchases.subscriptions API don't support test purchases.
So I go to the app page in play store...and do what exactly? the webpage does not state what i am supposed to do there. anyone know?
it does say: you can also use the cancel() method of the Purchases.subscriptions API.
which indicates that using the cancel() method is not the only method.
How to solve this without adding additional code in my app?
I went into the main Google Play Console page and clicked on Order Management. Under that I was able to select all test purchases and Refund them. I'm the primary developer of the app so I have access. If you are a tester you'd probably have to contact the support team and request that they refund your order.
All managed in-app products are consumable.
as stated in the docs.
That means that you can consume an owned item instead of cancelling the purchase and buy it all over again.
I suggest querying the inventory at the app launch time:
mIabHelper.queryInventoryAsync(this);
You can then consume the owned item in the callback:
#Override
public void onQueryInventoryFinished(IabResult result, Inventory inventory) {
Purchase purchase = inventory.getPurchase(MY_SKU);
boolean isBought = (purchase != null && verifyDeveloperPayload(purchase));
if (isBought) {
mIabHelper.consumeAsync(purchase, new OnConsumeFinishedListener() {
#Override
public void onConsumeFinished(Purchase purchase, IabResult result) {
//Clear the purchase info from persistent storage
}
});
}
}
This is OK for testing the IAB flow but make sure to remove this code from the release version.
On Play Console, go to Developer Account -> Account Details, set the license testers (you are one by default)
Purchase items
Go to Order Management, choose those test orders, select: refund, but DON'T FORGET to check REVOKE when you are refunding. (I forgot to revoke, and now can't find a way to take them back)
Anyway, another test account will do.
I found a solution which isn't very convenient, but works. It seems like you can consume uncomsumable products and that way you can buy them again. I'm working with phonegap so I only have example code for the cordova-plugin-purchase plugin:
store.when("your.product.id").updated(product => {
if(product.owned) {
var transaction = product.transaction;
product.transaction = null;
store.inappbilling.consumePurchase(
function() { // success
alert("consume success");
},
function(err, code) { // error
alert("consume error " + err)
},
product.id,
transaction.id
);
}
});
The updated callback gets called when you call store.refresh() or buy the product. So depending on your use case you'd want to implement an additional method of checking when to consume the product.
I have no experience with the native Android in-app payments, but obviously you will be able to consume the products there as well.
Edit: Sorry, I just read that you didn't want to include additional code in your project. I don't think that's possible at the moment, but would like to keep my answer here because it might help other people trying to test in-app payments.
What worked for me was a combination of both:
Go to order management and refund
clear cache/data in Play Store app (as well as your app in you placed some shared prefs).
Also, in case you get an item already owned status, you can consume the purchase using the purchase token and calling billingClient.consumeAsync().
Didn't find a solution for this.
My workaround is simply remove the current test user from the test users list, make a real purchase, then cancel it using the merchant console.
The queryPurchaseHistoryAsync method still finds test orders I've made over the last year, despite having long ago consumed, refunded, and revoked them. One precaution I've found helpful is clearing the data in the Google Play Store app (settings/apps/google play store/storage/clear data). queryPurchaseHistoryAsync pulls obsolete purchase data from here, although only the non-networking (and completely unreliable I've found) queryPurchases is supposed to do this. You may have to add additional code to your app after all, but it doesn't have to be much.
With the dropping of support for Trivial Drive 2 (the link in the docs takes you to a '404 page does not exist' error, the github files are archived, and updating to billing:2.1.0 will give you a vending import compile error in the IabHelper class), answers to this popular question involving IabHelper might be considered obsolete. Billing is a lot simpler now if you follow the basic code pieces in the docs starting here https://developer.android.com/google/play/billing/billing_overview with no messy helper classes. One persistent issue is a 'Both methods have same erasure, yet neither overides the other' method clash error you may run into with this implementation, see my solution here 'Both methods have same erasure, yet neither overides the other' method clash error in SkuDetailsResponseListener().
Once you have the newest billing code implemented, you can create a hidden trigger in your production app to call queryPurchaseHistoryAsync to get a purchaseHistoryRecordList. Then call consumeAsync for each item in this list. Here is the barebones code to consume all test orders, allowing multiple tests of your nonconsumables:
billingClient.queryPurchaseHistoryAsync(BillingClient.SkuType.INAPP,
new PurchaseHistoryResponseListener() {
#Override
public void onPurchaseHistoryResponse(BillingResult billingResult,
List<PurchaseHistoryRecord> purchaseHistoryRecordList){
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK
&& purchaseHistoryRecordList != null) {
for (PurchaseHistoryRecord purchaserecord : purchaseHistoryRecordList) {
if(purchaserecord!=null){
ConsumeParams consumeParams =
ConsumeParams.newBuilder()
.setPurchaseToken(purchaserecord.getPurchaseToken())
.setDeveloperPayload(purchaserecord.getDeveloperPayload())
.build();
billingClient.consumeAsync(consumeParams, consumelistener);
}}}}});
For people using a way based on the new TrivialDriveKotlin, consumable products are consumed during the installation of the app in the method
handleConsumablePurchasesAsync
If your purchase is not consumable, you can make it consumable by adding the corresponding sku into CONSUMABLE_SKUS in the GameSku object. Exemple:
val CONSUMABLE_SKUS = listOf(GAS, PREMIUM_CAR)
Then uninstall your app from your device and install it again, and your non consumable purchase is available again. Fast and simply.
Of course, don't forget to remove your
I set up a beta account to test IAP for google app that I am working on, the issue I have is, once I have purchased One-time products(non-recurring charge) the test IAP, I cannot 'remove it' as such, so now, even when I delete the app and re-install, it remembers the purchase, that's great in the real world for a user, but not great when trying to fix the bugs!
Is there any way (short of making a ton of gmail accounts to test with) to remove the purchase from the account?
This is an old question but if someone is still looking for a solution then go to:
Google Play console and open the Order Management tab
There you can refund / cancel test purchases. Then clear the purchase state using this command:
adb shell pm clear com.android.vending
The only way I know is to force a consume in your app. You can then remove that code.
I am using cc.fovea.cordova.purchase plugin for cordova to manage my IAP purchases. To get my test Non-Consumables to be deleted I changed my registration from Non-consumable to Consumable.
store.register({
id: this.predatorID,
alias: 'Predator Pack',
type: store.CONSUMABLE //store.NON_CONSUMABLE
});
Also, apparently there are reserved keywords you could use instead (if you're into that). - https://developer.android.com/google/play/billing/billing_testing.html
I encountered the same situation and started to research. Unfortunately, the directions made here did not produce a solution.
I want to share the solution that worked for me.
If you call the method below in the right place, the solution will be produced. Source : Link
/**
* Recall that Google Play Billing only supports two SKU types:
* [in-app products][BillingClient.SkuType.INAPP] and
* [subscriptions][BillingClient.SkuType.SUBS]. In-app products are actual items that a
* user can buy, such as a house or food; subscriptions refer to services that a user must
* pay for regularly, such as auto-insurance. Subscriptions are not consumable.
*
* Play Billing provides methods for consuming in-app products because they understand that
* apps may sell items that users will keep forever (i.e. never consume) such as a house,
* and consumable items that users will need to keep buying such as food. Nevertheless, Google
* Play leaves the distinction for which in-app products are consumable entirely up to you.
*
* If an app wants its users to be able to keep buying an item, it must call
* [BillingClient.consumeAsync] each time they buy it. This is because Google Play won't let
* users buy items that they've previously bought but haven't consumed. In Trivial Drive, for
* example, consumeAsync is called each time the user buys gas; otherwise they would never be
* able to buy gas or drive again once the tank becomes empty.
*/
private fun clearIapHistory() {
billingClient!!.queryPurchases(BillingClient.SkuType.INAPP).purchasesList
.forEach {
val params =
ConsumeParams.newBuilder().setPurchaseToken(it.purchaseToken).build()
billingClient!!.consumeAsync(params) { responseCode, purchaseToken ->
when (responseCode.responseCode) {
BillingClient.BillingResponseCode.OK -> {
}
else -> {
Log.w(LOG_TAG, responseCode.debugMessage)
}
}
}
}
}
if (inventory.getPurchase(ITEM_SKU) != null ) {
try {
mIabHelper.consumeAsync(premiumPurchase, new IabHelper.OnConsumeFinishedListener() {
#Override
public void onConsumeFinished(Purchase purchase, IabResult result) {
Toast.makeText(MainActivity.this, "Consumed the test purchase successfully", Toast.LENGTH_SHORT).show();
}
});
} catch (IabHelper.IabAsyncInProgressException e) {
e.printStackTrace();
}
}
However refund() and revoke() methods don't support test purchases and you are left with only consumeAsync() option.
Just:
Purchase unlockedPurchase = inventory.getPurchase(SKU_UNLOCKED);
// Log unlockedPurchase.getOrderId();
Go to your Google Play panel, Order management, look for that order id and refund it (it should say Test order if it is your own order).
I guess the only method working is to...
Consume It!
For further info, get to the consuming document and search for "consume": https://developer.android.com/google/play/billing/integrate
Here are the important steps for you:
Dependence setup.
Billing client connection.
Query the Purchase.
Consume(Purchase).
Good Luck~
Test non-consumable products
To perform multiple test purchases for the same non-consumable product, you can refund and revoke purchases using Google Play Console.
I had a similar issue. Fortunately, the app I'm working with is WebView-based, so I can easily inject a link or button to trigger some Javascript to call back into the application to consume the test orders. Since test orders have an empty string for the orderId, it is easy to identify them to consume them. Once consumed, the item can be "purchased" again. Removing the button requires commenting out one line of code BUT if the button accidentally makes it into the final published app, it won't cause any problems since the code only consumes test orders - that is, real orders are not affected. That button will just be embarrassing instead of a disaster.
I'm working on a device without a credit card associated with it. I set up some promo codes and use the "Redeem Code" option for my test orders. Promo codes result in no risk of money exchanging hands and I'm able to completely verify IAB functionality in my app with real products without having to resort to the IAB test codes.
Nothing shows up for me in Google Wallet as per the post by Martin Kool.
You can refund the test purchase of non-consumable product and it will work just fine.
Using the Play Console website
Open Play Console.
On All Apps page left side, select Order Management
Use the "Search orders" box to search by order ID or the user's full email address.
Open order, click on Refund button. Select Entitlement checkbox and refund.
More details here about refunds: https://support.google.com/googleplay/android-developer/answer/2741495
Google Play Purchases are stored in Google Wallet.
https://wallet.google.com
When signed, go to "Transactions" on the left. Test purchases can be cancelled from there.
I created a google app, I put it on the play store, and now I am testing inapp purchases with my test account. But then I cancel my purchase so to test how cancellations propagate. But cancellations are not reflected in the app. I can still use my test account with full access to the goods in question. I am using managed products.
It's a whole lot of code so it would be outrageous to post it all here. But I have reviewed my code over and over, and it's doing the right thing. In fact here is a snippet: when a user first launch the app, I check for outstanding purchase and change the state of the good in question as follows
Purchase premiumPurchase = inventory.getPurchase(SKU_PREMIUM);
boolean value = (premiumPurchase != null && verifyDeveloperPayload(premiumPurchase) && 0 == premiumPurchase.getPurchaseState());
setLockStateOfPremiumPurchase(this, value);
So I am now wondering if the cancellation has propagated to my app. In other words: how long does it take google to change their database so that when I check the inventory, it returns not purchased? Or perhaps I am doing this wrong: should I be checking for something else?
When I look at the original json purchase state is 0
After you cancelled the order in Google Wallet, you need to wait from 5 min to couple hours to get data synchronized back to your device. I'm not sure what it depends on. You just need to wait.
Sometimes it helps to restart the device after you received a confirmation e-mail. After restarting, the device registers itself in Google Play anew and purchases cache gets updated faster.