I'am using subscriptions in my application and it is working perfectly during testing. However, I didn't find a way to get user subscription history for all transactions.
Example:
-User subscribed to product id "sub1" for 3 months. (purchaseToken : "X")
-User canceled subscription for same product id "sub1"
-User resubscribed for same product id (purchaseToken : "Y")
In this scenario when querying queryPurchaseHistoryAsync() function it is returning only latest purchase. Also when using this [API][https://developers.google.com/android-publisher/api-ref/purchases/subscriptions/get] it returns only information of a specific purchase token ("Y" retreived from queryPurchaseHistoryAsync()).
Is there any other way to get user subscriptions history (Detailed transactions)
?
Any help would be greatly appreciated
After a lot of searching it turned out that there is no API that returns detailed transactions of specific subscription.
Related
Here are how my app handles auto-renew subscription
Launch billing flow with BillingClient 1.1 library (BillingClient.launchBillingFlow())
Send purchased detail to backend to handle premium things
Update premium status
If I failed at step 2 (connection error, server dead...), user exited app then open again, how to retrieve owned auto-renew subscription item to notify user that they already bought an item and to do step 2 again?
Thanks.
Answers my own question after 5 days without any answers.
To retrieve own auto-renew items, call BillingClient.queryPurchases(BillingClient.SkuType.SUBS) after onBillingClientSetupFinished() then check if is Purchase.isAutoRenewing() = true. Don't use queryPurchaseHistoryAsync() because Purchases return from this method don't have auto-renewing state (always return false).
This answer is valid for BillingClient v1.1.
I am developed an app in which user purchases will registered on the server. And on the server side OrderId of Purchase class is a primary key.
In android documentation, I have read that order Id will be blank for test purchases. However, my server will not accept blank order id. So I tried this workaround:
public static final boolean IS_TEST_PURCHASES = true;
// Before updating to server
if (IS_TEST_PURCHASES && Util.isBlank(purchaseId))
purchaseId = "Test." + TimeUtils.getCurrentUnixTime();
This solved my problem for some time. But, in Beta mode the same problem appeared as I cannot release APK with IS_TEST_PURCHASES = true.
I checked the JSON retrned after purchase in which only OrderId is blank and rest of the feilds like signature, token are available. I was thinking if I can make a condition like:
if OrderId is empty and Signature or Token is not empty, then this
purchase is a test purchase and assign some dummy order id to it.
But I am confused whether this condition will cause any future problems.
I need to differentiate real purchase and test purchase so that I can set some dummy OrderId if and only if purchase is made through sandbox testing.
Any help is appreciated.
I don't know if this will help you, but what I did to differentiate between a test purchase and a real purchase was to use both the orderId and the purchaseToken as keys.
If it is a real purchase, there will be an order ID present that follows the pattern of something like GPA.ORDER_ID. If it's a test purchase, you are right there will be no order ID, but there will still be a purchase token that is a long string of random numbers / letters. So I know in my DB if it was a real purchase, the order ID will have GPA.ORDER_ID. If it doesn't follow that pattern, then it was a test purchase.
I have an android app with renewable monthly subscriptions. In this app I want to notify user some info when his subcription continues in next month.
As I can see renewals in merchant center(orderId ends with eg. ..0, ..1), but when querying the inventory my purchase orderId is same as befor eq.
{
"orderId": "GPA.XXXX-YYYY-XXXX-ZZZZZ",
"packageName": "my.packageName",
"productId": "my.sku",
"purchaseTime": 1456398623654,
"purchaseState": 0,
"developerPayload": "mypayload",
"purchaseToken": "token",
"autoRenewing": true
}
What bothers me more is that purchaseTime also doesn't change.
So my question is: If there is any way to detect in app that renewal occured?
Edit:
I'm using Google Play Developer API to get subscription info and then calculate number of renewals myself.
Order id for all recurrences are returned in orderId field of the INAPP_PURCHASE_DATA JSON field (in V3) with each recurring transaction appended by an integer.
Subscription order numbers
To help you track transactions relating to a given subscription,
Google payments provides a base Merchant Order Number for all
recurrences of the subscription and denotes each recurring transaction
by appending an integer as follows:
GPA.1234-5678-9012-34567 (base order number)
GPA.1234-5678-9012-34567..0 (first recurrence orderID)
GPA.1234-5678-9012-34567..1 (second recurrence orderID)
GPA.1234-5678-9012-34567..2 (third recurrence orderID) ...
But due to local caching you might not get the latest information. So try clearing cache from application manager to first see if you get correct purchase information.
Since purchase query this way is not reliable, it makes more sense to call Google Play Developer Purchases.subscriptions: get API from a backend to get Purchases.subscriptions resource which will return expiryTimeMillis of current subscription.
{
"kind": "androidpublisher#subscriptionPurchase",
"startTimeMillis": long,
"expiryTimeMillis": long,
"autoRenewing": boolean,
"priceCurrencyCode": string,
"priceAmountMicros": long,
"countryCode": string,
"developerPayload": string,
"paymentState": integer,
"cancelReason": integer
}
The purchase data for subscriptions is returned only when the subscription is active.
If the subscription expires then you won't get this purchase data when you query the inventory.
Excerpt from the link
If a recurring payment fails (for example, because the customer’s
credit card has become invalid), the subscription does not renew. The
getPurchases() method does not return failed or expired subscriptions.
You can serve in your DB the expiration date, and every time, when you are getting the data, you can check the expiration date with your db's value, and if it is later, then the subscription was renewed))
I struggled to find a solution for the exact implementation of #random's suggestion. It seems to indeed be the only way to have a solid implementation for renewal tracking on Android, but I couldn't find a good approach online on how to do it. For those who want to save some time (cost me 6 hours today), please find my answer below:
1. First step is to add the dependencies:
implementation "com.google.apis:google-api-services-androidpublisher:v3-rev142-1.25.0" // Update based on latest release
implementation "com.google.auth:google-auth-library-oauth2-http:1.12.1" // Update based on latest release
2. Follow these steps to link the Google Play Console with Google Play Developer API (choose the "Use a service account", not "Use OAuth clients" and follow until "Additional information").
3. Download the services JSON file from your Google Cloud service account (click on the account that you set up in the previous step). You can find/create this file under the "Manage Keys" action or the "Keys" tab. Add the exported JSON file in your assets folder in Android
4. Then you can call the Google Play Developer API to query subscriptions like this (important to call from a Thread, didn't work from the UI thread, not sure why):
new Thread(() -> {
InputStream inputStream = context.getAssets().open("service_account_google_play.json"); // JSON file from step 3
GoogleCredentials credentials = GoogleCredentials.fromStream(inputStream)
.createScoped(AndroidPublisherScopes.ANDROIDPUBLISHER);
AndroidPublisher androidPublisher = new AndroidPublisher(
new NetHttpTransport(),
JacksonFactory.getDefaultInstance(),
new HttpCredentialsAdapter(credentials)
);
SubscriptionPurchase purchase = androidPublisher.purchases().subscriptions().get(
context.getPackageName(), subscriptionId, purchaseToken
).execute();
// Check the orderId or check the expiryTimeMillis for renewal check, e.g. purchase.getOrderId();
}).start();
At the risk of being overly descriptive, the subscriptionId is the ID of your subscription in the Play Console (e.g. subscription_monthly or whatever you called it), and the purchaseToken is the token you get from the Purchase token after querying the BillingClient (querying subscriptions is explained in detail here).
How to get Subscription details like Expiry date and Active status etc. I have completed buying subscription flow using
mHelper.launchSubscriptionPurchaseFlow((Activity) mContext, RegistrationAcceptActivity.SKU_SUBSCRIBE, RegistrationAcceptActivity.RC_REQUEST,
mPurchaseFinishedListener, "");
Once the it executed successfully, I got
{"orderId":"GPA.1380-1270-1169-98789","packageName":"com.xxx","productId":"xxxx","purchaseTime":1458222969093,"purchaseState":0,"purchaseToken":"aaaabbbbccccddddeeeeasdfsadfasdfsadfxcvxcvxcvxcsdfasdafsadfsd","autoRenewing":true}
But still I am searching for how to get subscription detail whenever needed. Ex: I need to send the updated expiry date to my server while splash activity loading.
Some one suggested to use Google Developer console(V2) API. Some of the classes are deprecated like HTTPDefaultClient etc.
I am unable to conclude whether this is the right solution or I need to follow some other way to get subscription details.
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.