I've implemented in-app billing from this tutorial. The item to buy is the ability to set a custom background. It works great, but when I uninstall and re-install the app (or clear the user prefs), I'm having trouble figuring out how to verify that somebody has already purchased the in-app item.
public void buySelected() {
if (backgroundColorsPurchased == true) {
this.colorChangeDialog(); //if user has already purchased, just call the dialog instead of re-buying.
//if the person has cleared their prefs, they'll have to be online to re-verify that they did indeed buy the item.
}else{
if(BillingHelper.isBillingSupported()){
BillingHelper.requestPurchase(mContext, "background.colors");
BillingHelper.setCompletedHandler(mTransactionHandler);
} else {
Log.i(TAG,"Can't purchase on this device");
}
}
}
Then I have the handler:
public Handler mTransactionHandler = new Handler(){
public void handleMessage(android.os.Message msg) {
Log.i(TAG, "Transaction complete");
Log.i(TAG, "Transaction status: "+BillingHelper.latestPurchase.purchaseState);
Log.i(TAG, "Item purchased is: "+BillingHelper.latestPurchase.productId);
if(BillingHelper.latestPurchase.isPurchased()){
//this is where we show the stuff that the person purchased. In this case, the dialog to change the background colour.
backgroundColorsPurchased = true; //just setting this to true so that the next time somebody clicks the donate button it'll just open the dialog.
//call the change background dialog
colorChangeDialog();
}else{
//fail
Toast.makeText(getApplicationContext(), "Fail", Toast.LENGTH_SHORT).show();
}
}
};
How am I able to verify that the item has been purchased before? The market just keeps popping a dialog window that says, "You have already purchased this item, or the purchase is still pending." When I try using something like if(BillingHelper.latestPurchase.isPurchased()){ I get a force close if it's not within the handler.
You need to call restoreTransactions take a look at the default example that Android has provided for In App Billing for more reference.
You need to restore transactions if the preferences/data have been cleared. That will send you information of the items the user has purchased, and you can process authorizations as usual. Read reference and check the Dungeons sample about how to do this.
you can simply use Fire-base login & realtime database with field premium_enabled - true/false (this will help if user goes for another mobile with old google id for Play-store)
step wise
add google login in your App.
make an entry of user in fire-base user_id/email_id and premium_enabled field and store data in it. (you can add fields as per your requirement)
if user purchased than make an entry premium_enabled - true & also use shared preference for offline check
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);
}
}
}
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.
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.
I'm very confused on how in-app billing works. I've read the documentation and I must have missed something because I don't understand the final step I need to implement into my application to make this work. The in-app billing works great, however, if a user uninstalls my app and installs it again at a future date, my application doesn't know how to determine if the in-app purchase has previously been made. Here's a snippet from my main class where I attempt to handle all of this:
#Override
public void onCreate(Bundle savedInstanceState)
{
mContext = this;
startService(new Intent(mContext, BillingService.class));
BillingHelper.setCompletedHandler(mTransactionHandler);
}
I am using the example classes from the dungeons example project. What I don't understand is how the below code works at the time of purchase, but re-running it doesn't work to check if something been purchased already. I have been stuck on this part for about a month now and I've been getting very frustrated with it.
public Handler mTransactionHandler = new Handler()
{
public void handleMessage(android.os.Message msg)
{
Log.i(TAG, "Transaction complete");
Log.i(TAG, "Transaction status: "
+ BillingHelper.latestPurchase.purchaseState);
Log.i(TAG, "Item purchased is: "
+ BillingHelper.latestPurchase.productId);
if (BillingHelper.latestPurchase.isPurchased())
{
new Message("Thank you!", getApplicationContext());
PAY_VERSION = true;
SharedPreferences purchasePreferences = getSharedPreferences(PURCHASE_PREFERENCES, Activity.MODE_PRIVATE);
Editor purchaseEditor = purchasePreferences.edit();
purchaseEditor.putBoolean("purchased", PAY_VERSION);
purchaseEditor.commit();
Intent intent = getIntent();
finish();
startActivity(intent);
}
};
What I need is some way to query the server to see if this item has been purchased or not. I understand that there's a PURCHASE_STATE_CHANGED thing there somewhere however I have no idea how to do anything when it determines the state has changed or how to initiate it to check. I'm lost and all I need is a good push in the right direction because so far I'm just completely lost.
EDIT:
I've also heard you need to parse JSON, but I have no idea how to even begin doing that.
EDIT 2:
Am I supposed to call this stuff to check?
BillingHelper.restoreTransactionInformation(BillingSecurity.generateNonce());
BillingHelper.getPurchaseInformation(new String[] {"myItem"});
That code previously had crashed on my sisters phone (SGS3, ICS) but not on mine (GN, ICS, and JB work). I was calling it in onCreate() of my first activity. Not really sure what to do with getPurchaseInformation(...) once it's been called. It has no return value so I'm not sure if I can parse the JSON or whatever I'm supposed to do...
Also, those 2 lines give me this:
08-27 11:54:04.271: E/BillingService(17702): BillingHelper not fully instantiated
08-27 11:54:04.271: E/BillingService(17702): BillingHelper not fully instantiated
JSON
An example of a JSON order object that includes a subscription purchase token is shown below.
{ "nonce" : 1836535032137741465,
"orders" :
[{ "notificationId" : "android.test.purchased",
"orderId" : "transactionId.android.test.purchased",
"packageName" : "com.example.dungeons",
"productId" : "android.test.purchased",
"developerPayload" : "bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ",
"purchaseTime" : 1290114783411,
"purchaseState" : 0,
"purchaseToken" : "rojeslcdyyiapnqcynkjyyjh" }]
}
How to parse JSON in Java.
Android supports JSON library
Android Billing
If you have a remote server, we recommend that you store purchase information on your server instead of in a local database on a device. Look here.
Read about Managed per user account, Unmanaged, or Subscription
I use billing like this:
In onCreate
BillingHelper.setCompletedHandler(handlerTransaction);
You have handler in your code.
Next in onClick or something
BillingHelper.requestPurchase(this, currentMarketProduct.getMarketId());
You are on the right track with the second update. The trick is to realise everything is done in async.
The typical flow is as follows:
User installs your app.
On first load of your app, you check if you need to restore
purchases.
If you do, send a RESTORE_TRANSACTION synchronous request to Google.
Google will respond with a acknowlegment response to your
RESTORE_TRANSACTION request. (This is only an acknowlegement that
they received your request.)
At this point, you should mark that you had already sent a restore request to Google and no further restores needs to be sent from the app.
Now asynchronously Google will start sending a 'PURCHASE_STATE_CHANGED' event to your app for each in-app purchase the user has previously purchased. This call is the same as what Google would had sent if the user had made that purchase for the first time.
Since it's the same call, your app would pick up the event and handled it normally as if the user has just purchased the in-app product (thereby "restoring" the purchased feature).
In regard to steps 2 and 5, what I've done for my app is to keep a SharedPreference value called 'APP_INITIALISED' that defaults to false. Everytime my app starts up, if 'APP_INITIALISED' is false, I tell Google to RESTORE_TRANSACTION (step 2) then I set APP_INITIALISED to true (step 5).
I am playing with the Google in-app billing example app "Dungeons".
In this app, I can either click on the "Buy" button to buy something or on the "Edit Payload" button to...edit the payload :). But I don't understand what does this button and what "Edit Payload" means... Can anyone clarify that ?
By the way, can anyone tell me how the "Buy" button fires the buying action as the code in the Dungeons app is the following (the buying action is launched by I don't understand how...):
public void onClick(View v) {
if (v == mBuyButton) {
// NO CODE HERE TO DO SOMETHING ???!!!
if (!mBillingService.requestPurchase(mSku, mPayloadContents)) {
showDialog(DIALOG_BILLING_NOT_SUPPORTED_ID);
}
} else if (v == mEditPayloadButton) {
showPayloadEditDialog();
}
}
Thanks !
A "Developer Payload" is a developer-specified string that contains supplemental information about an order. You can specify a value for this field when you make a REQUEST_PURCHASE request.
For example, you can use this key to send index keys with an order, which is useful if you are using a database to store purchase information. Google recommends that you do not use this key to send actual data or content.
For more info see the In-app Billing Reference.
in the if statment the requestPurchase method is called, this does the purchase request then if it fails, the error Dialog box is shown. From there you can also follow the payload down to see how it is used.