I implemented android in app billing, uploaded it in developer console for alpha tests, created an item (one time product for premium version) and tested on my device. Everything worked, but I wanted to test a second time with same device - isn't it possible to undo purchase?
What I tried:
I canceled the purchased item in developer console --> nothing happened on my device, BillingClient.getPurchaseList still returned my premium purchase
I cleared cache with ad command "adb shell pm clear com.android.vending" --> and now it's getting strange:
First, I thought it works and BillingClient.queryPurchases().getPurchaseList didn't return any purchase and my app behaviour changed correctly to basic version. But if I try to buy the item one more time to test the purchase flow again, it says "item already owned". Isn't there any possibility testing it again??
Another strange thing I absolutely don't understand: I didn't do anything, openend my app a few hours later again and it is marked as premium again. What does that mean? Is in a problem in test account or can that also happen in real (canceled) purchases??
Thanks a lot for your help!
If you want to allow an item to be purchased multiple times (i.e in-game currency), you should consume it before buying it again, otherwise the IAB library will return the "Item already owned" error.
To reset a purchase you can use BillingClient#consumeAsync(String purchaseToken).
To get the purchaseToken of a purchase, use BillingClient#queryPurchaseHistoryAsync, this will return the list of current purchases.
If you want to consume all purchases for debugging purposes, you can just use the following code:
client.queryPurchaseHistoryAsync(BillingClient.SkuType.INAPP, new PurchaseHistoryResponseListener() {
#Override
public void onPurchaseHistoryResponse(int responseCode, List<Purchase> purchasesList) {
if (purchasesList != null && !purchasesList.isEmpty()) {
for (Purchase purchase : purchasesList) {
client.consumeAsync(purchase.getPurchaseToken(), new ConsumeResponseListener() {
#Override
public void onConsumeResponse(int responseCode, String purchaseToken) {
if (responseCode == BillingResponse.OK) {
//Item consumed, you may repurchase it now
} else {
// Error, item not consumed. See responseCode for more info
}
}
});
}
}
}
});
Trying to clear cache won't fix the problem because as soon as the IAP library resyncs with GPlay, it will remember the purchases associated with the account of the current user.
Related
I'm having trouble figuring out how to detect when a refund has been issued for a managed (uncomsumable) in-app product in Android using com.android.billingclient:billing:2.0.3. The problem seems fairly deep though maybe I'm making it more complicated than it ought to be.
To begin, I've made a test purchase which has been acknowledged AND refunded:
Looking at the logs of my app I see the following:
D/BillingManager: Got a verified purchase: Purchase. Json: {"orderId":"GPA.3362-7185-5389-78416","packageName":"com.glan.input","productId":"pro","purchaseTime":1567672759460,"purchaseState":0,"purchaseToken":"pbkpcaadklleoecegjfjdpbl.AO-J1OwsR6WVaVZCCYOU6JyYN1r0qJsrwitIPZfhc3jX4yketRUwNzKqwMgYx0TgZ2GebEGbXDL0RlMyogwtSKSPsaHCJ4RA4MPlIGay-aM1-QhmnqwjXjQ","acknowledged":true}
I/BillingManager: purchase pbkpcaadklleoecegjfjdpbl.AO-J1OwsR6WVaVZCCYOU6JyYN1r0qJsrwitIPZfhc3jX4yketRUwNzKqwMgYx0TgZ2GebEGbXDL0RlMyogwtSKSPsaHCJ4RA4MPlIGay-aM1-QhmnqwjXjQ is in 1 state
There's something funny going on here:
We can see the order IDs match up between what's in the image and the detected purchase
The first log line is printing the purchase with Log.d(TAG, "Got a verified purchase: " + purchase); which is printing the underlying JSON which represents the purchase.
Note that "purchaseState":0
The second log line is issued with Log.i(TAG, "purchase " + purchase.getPurchaseToken() + " is in " + purchase.getPurchaseState() + " state");.
Note that here purchase.getPurchaseState() is resulting in a value of 1
If I look at the implementation of getPurchaseState in Android Studio I see the following:
public #PurchaseState int getPurchaseState() {
switch (mParsedJson.optInt("purchaseState", PurchaseState.PURCHASED)) {
case 4:
return PurchaseState.PENDING;
default:
return PurchaseState.PURCHASED;
}
}
Earlier in the file the PurchaseState interface is declared as:
#Retention(SOURCE)
public #interface PurchaseState {
// Purchase with unknown state.
int UNSPECIFIED_STATE = 0;
// Purchase is completed.
int PURCHASED = 1;
// Purchase is waiting for payment completion.
int PENDING = 2;
}
It seems like getPurchaseState never returns PurchaseState.UNSPECIFIED_STATE and only returns PENDING which the JSON comes with a value of 4. I've confirmed that a state of PENDING is correctly returned when the purchase is performed with a payment method that takes a while to approve.
I've found posts like In-App Billing v3 - Don't detect refund which suggest that Play Services are caching purchases but I'm not convinced that's causing this problem because if I modify my code betweens runs of my app to acknowledge/consume the purchase those get state changes get immediately reflected in the JSON of the purchase.
How am I supposed to detect a refunded managed product?
I have one purchase (SkuType.INAPP) in my application. I make a test purchase and then make a refund.
Problem:
purchase.getOriginalJson() // contains "purchaseState":0
purchase.getPurchaseState() // returns 1
Inside com.android.billingclient.api.Purchase:
public int getPurchaseState() {
switch(this.zzc.optInt("purchaseState", 1)) {
case 4:
return 2;
default:
return 1;
}
}
//...
public #interface PurchaseState {
int UNSPECIFIED_STATE = 0;
int PURCHASED = 1;
int PENDING = 2;
}
Hacky way to check purchaseState from original json:
purchase.getOriginalJson().contains(String.format("\"purchaseState\":%s", Purchase.PurchaseState.PURCHASED))
Unfortunately, this problem still exists!
More details here.
You can check if still purchase exits following
binding.btnRestore.setOnClickListener(v->{
Purchase.PurchasesResult purchasesResult = mBillingClient.queryPurchases(BillingClient.SkuType.INAPP);
for ( Purchase purchase : purchasesResult.getPurchasesList()){
handlePurchase(purchase);
}
});
Google Play returns the purchases made by the user account logged in to the device. If the request is successful, the Play Billing Library stores the query results in a List of Purchase objects.
Note: Only active subscriptions appear on this list. As long as the in-app product is on this list, the user should have access to it. For further information, see the Handle SUBSCRPTION_ON_HOLD section of Add subscription-specific features.
To retrieve the list, call getPurchasesList() on the PurchasesResult. You can then call a variety of methods on the Purchase object to view relevant information about the item, such as its purchase state or time. To view the types of product detail information that are available, see the list of methods in the Purchase class.
You should call queryPurchases() at least twice in your code:
Call queryPurchases() every time your app launches so that you can restore any purchases that a user has made since the app last stopped.
Call queryPurchases() in your onResume() method, because a user can make a purchase when your app is in the background (for example, redeeming a promo code in the Google Play Store app).
Calling queryPurchases() on startup and resume guarantees that your app finds out about all purchases and redemptions the user may have made while the app wasn't running. Furthermore, if a user makes a purchase while the app is running and your app misses it for any reason, your app still finds out about the purchase the next time the activity resumes and calls queryPurchases().
Query most recent purchases
The queryPurchases() method uses a cache of the Google Play Store app without initiating a network request. If you need to check the most recent purchase made by the user for each product ID, you can use queryPurchaseHistoryAsync(), passing the purchase type and a PurchaseHistoryResponseListener to handle the query result.
queryPurchaseHistoryAsync() returns a PurchaseHistory object that contains info about the most recent purchase made by the user for each product ID, even if that purchase is expired, cancelled, or consumed. Use queryPurchases() whenever possible, as it uses the local cache, instead of queryPurchaseHistoryAsync(). If using queryPurchaseHistoryAsync(), you can also combine it with a Refresh button, allowing users to update their list of purchases.
The following code demonstrates how you can override the onPurchaseHistoryResponse() method:
private void handlePurchase(Purchase purchase) {
if(purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
if (purchase.getSku().equals(skuPro)) {
EntityPRO entityPRO = RoomDB.getDatabase(context).proDAO().getLastItem();
entityPRO.isBought = true;
RoomDB.getDatabase(context).proDAO().updateSpecificSLI(entityPRO);
Toast.makeText(context, context.getString(R.string.pro_succesfully_bought), Toast.LENGTH_LONG).show();
}
if (!purchase.isAcknowledged()) {
AcknowledgePurchaseParams acknowledgePurchaseParams =
AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();
mBillingClient.acknowledgePurchase(acknowledgePurchaseParams, acknowledgePurchaseResponseListener);
}
}
}
In my application I offer the user to make a donation using Google Play IAP, in return I remove ads and unlock premium features.
When my application loads I want to check if user made a donation, how to do that via code knowing that after user makes a donation I'll call the following code to allow the user to make future donations if desired.
So, I want to allow the user to make further donations if desired, but I want to know if (s)he already made a donation to disable ads and unlock premium features.
BillingProcessor bp;
bp.consumePurchase(productId);
Note, my questions is about IAP online process not about saving a value offline and check it later.
I think this guide should help show you how to do this:
https://developer.android.com/google/play/billing/billing_library_overview
Query cached purchases
To retrieve information about purchases that a
user makes from your app, call the queryPurchases() method with the
purchase type (SkuType.INAPP or SkuType.SUBS) on the Play Billing
Library client. For example:
PurchasesResult purchasesResult = mBillingClient.queryPurchases(SkuType.INAPP);
Google Play returns the
purchases made by the user account logged in to the device. If the
request is successful, the Play Billing Library stores the query
results in a List of Purchase objects.
Note: Only active subscriptions appear on this list. As long as the
in-app product is on this list, the user should have access to it. For
further information, refer to Handle SUBSCRPTION_ON_HOLD section of
the Add subscription-specific features document. To retrieve the list,
call the getPurchasesList() method on the PurchasesResult object. You
can then call a variety of methods on the Purchase object to view
relevant information about the item, such as its purchase state or
time. To view the types of product detail information that are
available, see the list of methods in the Purchase class.
Call queryPurchases() at least twice in your code:
Every time your app launches so that you can restore any purchases
that a user has made since the app last stopped. In your onResume()
method because a user can make a purchase when your app is in the
background (for example, redeeming a promo code in Play Store app).
Calling queryPurchases() on startup and resume guarantees that your
app finds out about all purchases and redemptions the user may have
made while the app wasn't running. Furthermore, if a user makes a
purchase while the app is running and your app misses it for any
reason, your app still finds out about the purchase the next time the
activity resumes and calls queryPurchases().
Query most recent purchases
The queryPurchases() method uses a cache
of the Google Play Store app without initiating a network request. If
you need to check the most recent purchase made by the user for each
product ID, you can use the queryPurchaseHistoryAsync() method and
pass the purchase type and a PurchaseHistoryResponseListener to handle
the query result.
queryPurchaseHistoryAsync() returns the most recent purchase made by
the user for each product ID, even if that purchase is expired,
cancelled, or consumed. Use the queryPurchases() method whenever
possible, as it uses the local cache, instead of the
queryPurchaseHistoryAsync() method. You could combine
queryPurchaseHistoryAsync() with a Refresh button allowing users to
update their list of purchases.
The following code demonstrates how you can override the
onPurchaseHistoryResponse() method:
mBillingClient.queryPurchaseHistoryAsync(SkuType.INAPP,
new PurchaseHistoryResponseListener() {
#Override
public void onPurchaseHistoryResponse(#BillingResponse int responseCode,
List purchasesList) {
if (responseCode == BillingResponse.OK
&& purchasesList != null) {
for (Purchase purchase : purchasesList) {
// Process the result.
}
}
} });
You can use this:
Purchase.PurchasesResult purchasesResult = billingClient.queryPurchases(BillingClient.SkuType.SUBS); //Or SkuType.INAPP
if (purchasesResult.getPurchasesList() != null) {
for (Purchase purchase : purchasesResult.getPurchasesList()) {
if (purchase.getSku().equals("your_product_id")) handlePurchase(purchase);
}
[...]
void handlePurchase(Purchase purchase) {
if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
premium = true; //In casse purchase was acknowledge before
if (!purchase.isAcknowledged()) {
AcknowledgePurchaseParams acknowledgePurchaseParams =
AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();
AcknowledgePurchaseResponseListener acknowledgePurchaseResponseListener = new AcknowledgePurchaseResponseListener() {
#Override
public void onAcknowledgePurchaseResponse(BillingResult billingResult) {
premium = true;
}
};
billingClient.acknowledgePurchase(acknowledgePurchaseParams, acknowledgePurchaseResponseListener);
}
}
}
If you have any questions, go ahead and comment.
As nasch and AlexBSC already answered, you have to fetch for possible made purchases.
However, the most up to date method of doing this is calling BillingClient.queryPurchasesAsync() as described in here. You should at least call it in onResume() and onCreate().
for example like this,
billingClient.queryPurchasesAsync(BillingClient.SkuType.SUBS, new PurchasesResponseListener() {
#Override
public void onQueryPurchasesResponse(BillingResult billingResult, List<Purchase> purchases) {
if (billingResult.getResponseCode() == OK
&& purchases != null) {
for (Purchase purchase : purchases) {
handlePurchase(purchase);
}
}
}
});
Following these steps should get you pretty far.
I implemented an In-App Purchase with the Play Billing Library 1.0 according to Google's tutorial.
I have only 1 item for purchase and when it gets unlocked, I show a Toast message with the length Toast.LENGTH_SHORT. However, the Toast stays there for like 10 seconds, so I assume it gets called multiple times. It does NOT happen when I unlock it via queryPurchases (if someone bought it earlier and reinstalled the app in the meantime).
Anyone have an idea why the Toast stays so long / why it gets called multiple times?
Inside my BillingManager class:
#Override
public void onPurchasesUpdated(int responseCode, #Nullable List<Purchase> purchases) {
if (responseCode == BillingClient.BillingResponse.OK) {
for (Purchase purchase : purchases) {
handlePurchases(purchase);
}
mBillingUpdatesListener.onPurchasesUpdated(mPurchases);
} else if (responseCode == BillingClient.BillingResponse.USER_CANCELED) {
} else {
}
}
public void handlePurchases(Purchase purchase) {
//here could be validation on own server
mPurchases.add(purchase);
}
Main Activity implements BillingUpdatesListener:
#Override
public void onPurchasesUpdated(List<Purchase> purchases) {
for (Purchase purchase : purchases) {
switch (purchase.getSku()) {
case "premium":
unlockPremium();
break;
}
}
}
public void unlockPremium() {
mPremiumUnlocked = true;
savePremiumUnlocked();
Toast.makeText(this, getResources().getString(R.string.premium_congrats), Toast.LENGTH_SHORT).show();
mAdView.setVisibility(GONE);
}
If I understand your correctly, you say that when you first purchase the in-app product, you are getting multiple Toasts?
In the current version (1.0) of the Billing library, this happens because multiple broadcasts are being made by the system.
For example, if you look at or breakpoint onPurchaseFinishedReceiver at line 120 in BillingClientImpl.java within the library, this is called at least twice after making a purchase. Both times, the in-app purchase data is attached but I noticed that the intent Action was different for each broadcast.
In the first broadcast, the Action was com.android.vending.billing.PURCHASES_UPDATED but in the second it was proxy_activity_response_intent_action. The library does not filter out the Action values and so all of these broadcasts result in your purchasesUpdatedListener being called.
I didn't investigate further but I think what we can take from this is that SOME sort of change occurred and it was deemed necessary to broadcast that change.
To avoid your multiple toasts, just do not display the toast unless your Premium functionality is unlocked. i.e. If it is already unlocked, simply ignore the change notification.
By the way, it is totally possible to debug the purchase flow in Android Studio. Just sign your debug apk with your release key and make sure the apk version is not higher than the one in Play Store.
buildTypes {
debug {
minifyEnabled false
debuggable true
signingConfig signingConfigs.release
}
release {
minifyEnabled false
signingConfig signingConfigs.release
}
}
I don't know if this applies to your exact scenario, but we are experiencing the same thing and it is a bug on Google's end.
See https://issuetracker.google.com/issues/66054158 for more information.
Edit: I just saw that #goRGon posted the same thing :)
The example of multiple people in Spain isn't the same situation as what's described above. In the Spain scenario, the users are actually purchasing two copies of the IAP, so they are two separate receipts and the users should be rewarded with two copies of whatever they purchased. In the bug scenario, one single receipt is presented to the user twice so duplicates can actually be caught. But either way, back-end validation systems need to accommodate hackers/bugs in code that might cause the same receipt to be sent twice in a row.
If your subscription activity was closed and reopened multiple times within the same application process, then onPurchasesUpdated may be called multiple times if the PurchasesUpdatedListener somehow stays attached to the previous instances of billingClient and if the previous connections are still alive. I noticed that the number of times I closed and reopened the subscription activity, onPurchasesUpdated was called for the same amount after a successful launchBillingFlow.
To fix this I needed to end the connection when the activity is destroying, like this -
#Override
protected void onDestroy() {
super.onDestroy();
if (billingClient!= null) {
billingClient.endConnection();
}
}
Great answer and looking deep by #Kuffs!
Google will fix multiple calls soon: https://issuetracker.google.com/issues/66054158
However, your integration with billing flow should work even when onPurchasesUpdate was triggered multiple times, since it could happen anyway. For example, if somebody was buying in parallel on another device with the same #gmail account. And people in some countries (e.g. Spain) do share their #gmail accounts rather frequently with many friends and family members.
Please, check TrivialDrive_v2 implementation to get an idea, how handle such situations gracefully.
I hope It will help you
_purchaseUpdatedSubscription =
FlutterInappPurchase.purchaseUpdated.listen((productItem) {
print('purchase-updated: ${productItem}');
getDetails(productItem);
});
String orderId = '';
getDetails(PurchasedItem purchasedItem) {
if (purchasedItem != null) {
if (orderId != purchasedItem.orderId) {
orderId = purchasedItem.orderId;
print('productItem.transactionReceipt : ${purchasedItem.transactionReceipt}');
var decodedData = jsonDecode(purchasedItem.transactionReceipt);
print('purchaseState : ${decodedData['purchaseTime']}');
if (decodedData['purchaseState'] == 0) {
if(purchasedItem.productId == selected_package) {
print("Purchased successfully");
onPurchased(purchasedItem);
}
} else {
ShowMsg('Transaction Failed !, Something went wrong.');
}
}
}
}
I'm facing one issue on Android InApp billing v3. I have created a managed inapp product in playstore and I need to buy that multiple times from multiple devices. For that, I am testing the app in two different devices. I'm not keeping any purchase data on local server. So whenever I need to purchase that item again I'll query the item to get the purchase status and based on that if the item is purchased I'll consume the item and will call the purchase function again. I'm detailing my error scenario below
On the first devices, I have done with the payment and I got the success result.
On that same device if I query for purchase detail, it will give the exact info.
On the same time if I query on the second device. its gives the inventory.hasPurchase(sku) as false and inventory.getPurchase(sku) as null.
While googling, I found that this is something related to Google playstore cache and all. After doing that, cache clearing manually, I getting the result as expected. Could anybody guide me on the above mentioned issue.
IabHelper.QueryInventoryFinishedListener mGotInventoryListener = new IabHelper.QueryInventoryFinishedListener() {
public void onQueryInventoryFinished(IabResult result, Inventory inventory) {
if (result.isFailure()) {
invokePurchaseError(ResponseCode.FAILED_TO_QUERY_INVENTORY, result.toString());
}
else{
if(mActionCode == RC_REQUEST_FOR_QUERYING){
boolean mIsPurchased = false;
Log.e("hasPurchase", inventory.hasPurchase(mSku)+"");
Log.e("getPurchase", inventory.getPurchase(mSku)+"");
if( inventory.hasPurchase(mSku)){
mPurchaseStatus = inventory.getPurchase(mSku);
mIsPurchased = (mPurchaseStatus != null && verifyDeveloperPayload(mPurchaseStatus));
if(mPurchaseStatus!=null){
mPurchaseStatus.getOriginalJson());
}
else{
//Not purchased
}
}
}
}
};
I had the same problem: Bought managed item on first device but second and third ones didn't seem to "know" about it. For me the solution was simple although:
As a first step I cleared the PlayStore-caches on all involved devices (not sure it is was necessary though..)
Secondly I bought the item again on the first device, but again the other ones seemed to fail getting the information. I gave the whole "system" some time (about 10-15minutes) and performed a re-check on device 2 and 3. And - oh wonder! - they finally got the correct hasPurchase = true information.
I'm on the edge of finishing my first app, and one last remaining thing is to implement IAP billing, so that's why I am currently reading quite a lot about the topic (including security concerns like encryption, obfuscation and stuff).
My app is a free version, with the ability to upgrade to full verison via IAP, so there would be just one managed purchase item "premium". I have a few questions about this:
In the Google IAP API example (trivialdrivesample), there's always the IAP check in MainActivity to see if the user bought the premium version, done via
mHelper.queryInventoryAsync(mGotInventoryListener);
My first concern:
This does mean that the user always needs to have an internet/data connection at app-startup, to be able switch to the premium version right? What if the user doesn't have an internet connection? He would go with the lite version I guess, which I would find annoying.
So I thought about how to save the isPremium status locally, either in the SharedPrefs or in the app database. Now, I know you can't stop a hacker to reverse engineer the app, no matter what, even so because I don't own a server to do some server-side validation.
Nevertheless, one simply can't save an "isPremium" flag somewhere, since that would be too easy to spot.
So I was thinking about something like this:
User buys Premium
App gets the IMEI/Device-ID and XOR encodes it with a hardcoded String key, saves that locally in the app database.
Now when the user starts the app again:
App gets encoded String from database, decodes it and checks if decodedString == IMEI. If yes -> premium
If no, then the normal queryInventoryAsync will be called to see if the user bought premium.
What do you think about that approach? I know it's not supersecure, but for me it's more important that the user isn't annoyed (like with mandatory internet connection), than that the app will be unhackable (which is impossible anyway).
Do you have some other tips?
Another thing, which I currently don't have a clue about, is how to restore the transaction status when the user uninstalls/reinstalls the app. I know the API has some mechanism for that, and aditionally my database can be exported and imported through the app (so the encoded isPremium flag would be exportable/importable as well). Ok, I guess that would be another question, when the time is right ;-)
Any thoughts and comments to this approach are welcome, do you think that's a good solution? Or am I missing something/heading into some wrong direction?
I too was making the same investigations, but during my testing I figured out that you do not need to store it, as Google do all the caching you need and I suspect (though I have not investigated it) that they are doing so as securely as possible (seeing as it in their interest too!)
So here is what i do
// Done in onCreate
mHelper = new IabHelper(this, getPublicKey());
mHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() {
public void onIabSetupFinished(IabResult result) {
if (!result.isSuccess()) {
// Oh noes, there was a problem.
Log("Problem setting up In-app Billing: " + result);
} else {
Log("onIabSetupFinished " + result.getResponse());
mHelper.queryInventoryAsync(mGotInventoryListener);
}
}
});
// Called by button press
private void buyProUpgrade() {
mHelper.launchPurchaseFlow(this, "android.test.purchased", 10001,
mPurchaseFinishedListener, ((TelephonyManager)this.getSystemService(Context.TELEPHONY_SERVICE)).getDeviceId());
}
// Get purchase response
private IabHelper.OnIabPurchaseFinishedListener mPurchaseFinishedListener = new IabHelper.OnIabPurchaseFinishedListener() {
public void onIabPurchaseFinished(IabResult result, Purchase purchase)
{
if (result.isFailure()) {
Log("Error purchasing: " + result);
return;
}
else if (purchase.getSku().equals("android.test.purchased")) {
Log("onIabPurchaseFinished GOT A RESPONSE.");
mHelper.queryInventoryAsync(mGotInventoryListener);
}
}
};
// Get already purchased response
private IabHelper.QueryInventoryFinishedListener mGotInventoryListener = new IabHelper.QueryInventoryFinishedListener() {
public void onQueryInventoryFinished(IabResult result,
Inventory inventory) {
if (result.isFailure()) {
// handle error here
Log("Error checking inventory: " + result);
}
else {
// does the user have the premium upgrade?
mIsPremium = inventory.hasPurchase("android.test.purchased");
setTheme();
Log("onQueryInventoryFinished GOT A RESPONSE (" + mIsPremium + ").");
}
}
};
So what happens here?
The IAB is set up and calls startSetup, on a successful completion (as long as it has been run once with an internet connection and is set up correctly it will always succeed) we call queryInventoryAsync to find out what is already purchased (again if this has been called while online it always works while offline).
So if a purchase is completed successfully (as can only be done while online) we call queryInventoryAsync to ensure that it has been called while online.
Now there is no need to store anything to do with purchases and makes your app a lot less hackable.
I have tested this many ways, flight mode, turning the devices off an on again and the only thing that messes it up is clearing data in some of the Google apps on the phone (Not likely to happen!).
Please contribute to this if you have different experiences, my app is still in early testing stage.
I refactored ne0's answer into a static method, including the comments from snark.
I call this method when my app starts - you'll need to enable your features at the TODO
/**
* This is how you check with Google if the user previously purchased a non-consumable IAP
* #param context App Context
*/
public static void queryPlayStoreForPurchases(Context context)
{
final IabHelper helper = new IabHelper(context, getPublicKey());
helper.startSetup(new IabHelper.OnIabSetupFinishedListener()
{
public void onIabSetupFinished(IabResult result)
{
if (!result.isSuccess())
{
Log.d("InApp", "In-app Billing setup failed: " + result);
}
else
{
helper.queryInventoryAsync(false, new IabHelper.QueryInventoryFinishedListener()
{
public void onQueryInventoryFinished(IabResult result, Inventory inventory)
{
// If the user has IAP'd the Pro version, let 'em have it.
if (inventory.hasPurchase(PRO_VERSION_SKU))
{
//TODO: ENABLE YOUR PRO FEATURES!!
Log.d("IAP Check", "IAP Feature enabled!");
}
else
{
Log.d("IAP Check", "User has not purchased Pro version, not enabling features.");
}
}
});
}
}
});
}
This will work across reboots and without a network connection, provided the user purchased the item.
Since you already know that it's impossible to make it unhackable using this system, I would recommend not attempting to prevent hacking. What you are proposing is known as "Security through obscurity" and is usually a bad idea.
My advice would be to try queryInventoryAsync() first, and only check your 'isPremium' flag if there is no internet connection.
There are also a few potential other ways of going about this, such as having separate free and premium apps, instead of an in app purchase. How other people handle this and the tools Google makes available might warrant an investigation.
queryInventoryAsync will automatically take into account uninstall and reinstalls, as it tracks purchases for the logged in user.
Yes the purchases can be retrieved offline. Also, I'm thinking about counting how many times the user opens the app as a mechanism before showing the billing UI.