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.');
}
}
}
}
Related
I use Google Play in-app in my app based the official sample project.
The Code A is to handle non-consumable products, it works well when I launch it using com.android.billingclient:billing-ktx:3.0.3 .
After I upgrade the project from Google Play Billing Library 3 to 4, I find the code purchase.sku doesn't work, so I have to replace it with purchase.skus.
The code of purchase.skus can be compiled in com.android.billingclient:billing-ktx:4.0.0, but I can't get the correct order, the test purchase is refunded after 3 minutes, it seems that Google Play doesn't acknowledge the purchase.
How can I fix the Code A when I upgrade Google Play Billing Library 3 to 4 ?
Code A
private fun processPurchases(purchasesResult: Set<Purchase>) {
val validPurchases = HashSet<Purchase>(purchasesResult.size)
purchasesResult.forEach { purchase ->
if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
if (purchase.sku.equals(purchaseItem)) {
//if (purchase.skus.equals(purchaseItem)) { //sku -> skus in 4.0
if (isSignatureValid(purchase)) {
validPurchases.add(purchase)
}
}
} else if (purchase.purchaseState == Purchase.PurchaseState.PENDING) {
Log.d(LOG_TAG, "Received a pending purchase of SKU: ${purchase.sku}")
// handle pending purchases, e.g. confirm with users about the pending
// purchases, prompt them to complete it, etc.
mContext.toast(R.string.msgOrderPending)
} else {
mContext.toast(R.string.msgOrderError)
}
}
acknowledgeNonConsumablePurchasesAsync(validPurchases.toList())
}
I'm not sure what is the exact reason of changing this method but I think that's probably because of the new subscriptions model in Google Play. Moreover this new method gets deprecated in the 5.0.0 version.
However, since the purchase.skus became a List of Strings, you could just check for your purchaseItem inside it. I think it depends on your purchases setup. Assuming your purchaseItem is also a String, you could either find your purchaseItem there:
if (purchase.skus.any { it == purchaseItem })
or simply take the first one to compare:
if (purchase.skus[0] == purchaseItem)
Of course, you should debug it to check what's exactly inside the skus list and then choose the best way to fix that.
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 have the serverless android app with simple functional: if user has some in-app subscription (auto renewable), then he can use functional in app, otherwise there is no. I know, how to make functional with obtaining subscriptions info (price, title etc) and calling payment. But I can not check if current user has active (not cancelled) subscriptions. I read so many information on many sites and tutorials, and there was written that I must use google API in my server. But I do not have my own server.
I used two different libraries for in-app subscriptions:
'com.anjlab.android.iab.v3:library:1.0.44'
and
'com.android.billingclient:billing:1.1'
but no one helped me for checking if user has active subscriptions. So, how to make this task? Help me please, maybe I missed some information...
Edit: Anjlab library hasn't been updated in the longest time ever. Kindly use Google's own billing library, this step-by-step process should help you easily integrate it into your app - Google In App Billing library
Using the anjlab in-app-billing library I was also facing the similar. This is what I did to get around it.
Invoke the method billingProcessor.loadOwnedPurchasesFromGoogle(); Then check the value of transactionDetails, if the TransactionDetails object return null it means, that the user did not subscribe or cancelled their subscription, otherwise they are still subscribed.
void checkIfUserIsSusbcribed(){
Boolean purchaseResult = billingProcessor.loadOwnedPurchasesFromGoogle();
if(purchaseResult){
TransactionDetails subscriptionTransactionDetails = billingProcessor.getSubscriptionTransactionDetails(YOUR_SUBSCRIPTION_ID);
if(subscriptionTransactionDetails!=null)
//User is still subscribed
else
//Not subscribed
}
}
Also, point to note is that the TransactionDetails object will only return null after the period of the subscription has expired.
Have you try to call bp.loadOwnedPurchasesFromGoogle(); ?
Edit
So try this :
Purchase purchase = inventory.getPurchase(product);
Log.d(TAG, "Purchase state: " + purchase.getPurchaseState());
// 0 (purchased), 1 (canceled), or 2 (refunded).
if (purchase.getPurchaseState() == 0
|| purchase.getPurchaseState() == 2) {
showPremiumVersion();
} else {
showFreeVersion();
}
Or this solution :
bp.isPurchased("yourSKU")
The isPurchsed method can't catch history of error's purchase / canceled Purchase/ Retrived Purchase.
Use this and don't forget to add INTERNET permission in your manifest.
TransactionDetails transactionDetails = billingProcessor.getPurchaseTransactionDetails("productId");
if (transactionDetails != null) {
//Already purchased
//You may save this as boolean in the SharedPreferences.
}
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.