I am implementing IAP in my app. One is for removing ad and other is for adding more puzzle. I was testing that on my device but came across an issue. After buying an item, I am getting response code "Item already owned" but it is not showing in purchase list.
I am setting up my billing client like this,
private void setUpBillingClient(){
mBillingClient = BillingClient.newBuilder(this).setListener(this).build();
mBillingClient.startConnection(new BillingClientStateListener() {
#Override
public void onBillingSetupFinished(#BillingClient.BillingResponse int billingResponseCode) {
if (billingResponseCode == BillingClient.BillingResponse.OK) {
List skuList = new ArrayList<>();
skuList.add(ITEM_SKU_MORE_PUZZLE);
skuList.add(ITEM_SKU_REMOVE_AD);
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP);
mBillingClient.querySkuDetailsAsync(params.build(),
new SkuDetailsResponseListener() {
#Override
public void onSkuDetailsResponse(int responseCode, List skuDetailsList) {
// Process the result.
if (responseCode == BillingClient.BillingResponse.OK
&& skuDetailsList != null) {
for (Object skuDetailsObject : skuDetailsList) {
SkuDetails skuDetails = (SkuDetails) skuDetailsObject;
String sku = skuDetails.getSku();
String price = skuDetails.getPrice();
if (ITEM_SKU_MORE_PUZZLE.equals(sku)) {
btnMorePuzzle.setText(price);
}
else if(ITEM_SKU_REMOVE_AD.equals(sku)) {
btnRemoveAd.setText(price);
}
}
}
}
});
}
}
#Override
public void onBillingServiceDisconnected() {
//Toast.makeText(getApplicationContext(), getResources().getString(R.string.billing_connection_failure), Toast.LENGTH_SHORT);
}
});
queryPurchases();
queryPrefPurchases();
}
First question, why is Billing response is OK here when I have already purchased the item. I don't want to set text of button as price, which is getting set from this response after product is bought.
This is my Onpurchase implementation,
#Override
public void onPurchasesUpdated(int responseCode, #Nullable List<Purchase> purchases) {
if (responseCode == BillingClient.BillingResponse.OK && purchases != null) {
for (Purchase purchase : purchases) {
if (purchase.getSku().equals(ITEM_SKU_REMOVE_AD)) {
mSharedPreferences.edit().putBoolean("ad_free", true).commit();
btnRemoveAd.setText("Done");
btnRemoveAd.setEnabled(false);
}
else if(purchase.getSku().equals(ITEM_SKU_MORE_PUZZLE)){
mSharedPreferences.edit().putBoolean("more_puzzle", true).commit();
btnMorePuzzle.setText("Done");
btnMorePuzzle.setEnabled(false);
}
}
} else if (responseCode == BillingClient.BillingResponse.ITEM_ALREADY_OWNED ) {
// I am getting response "Item already owned" here for item bought but purchase list here is empty
// so i can't do anything for purchased item
}
}
Second question, here I am getting response that my item is already bought but still list is empty.
How to implement it properly?
If someone already bought a product then button should be disabled.
Another doubt is while testing do I have to wait for 1-2 hrs to get that item refunded from playstore to test again or is there any other method.
I am following this code for in-app implementation.
https://github.com/patpatchpatrick/Streakr/
There is only one thing to point out here, that you are querying only the SKU type INAPP items only not the SUBS. I think the type of product you are providing comes under subscriptions not under in-app products which are used to be consumed. That is why your query is empty.
I found a mistake in your onPurchasesUpdated, conditions are set wrongly.
It should go something like this,
if(BillingResponse.OK && purchases != null) {
// update records
} else if(BillingResponse.ITEM_NOT_OWNED){ //this condition was missing
// update records if required or ask to buy
} else if(BillingResponse.ITEM_ALREADY_OWNED ){ // update records}
and i also suggest to update db for this.
Related
I have kept a donate tab and want to let the users buy the items over and over again. I have implemented a code but it lets the user buy the specific item only once. I have used managed products in play console for products.
btn.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if(billingClient.isReady()){
SkuDetailsParams params=SkuDetailsParams.newBuilder()
.setSkusList(Arrays.asList("purchase_aaa","purchase_bbb","purchase_ccc","purchase_ddd"))
.setType(BillingClient.SkuType.INAPP).build();
billingClient.querySkuDetailsAsync(params, new SkuDetailsResponseListener() {
#Override
public void onSkuDetailsResponse(int responseCode, List<SkuDetails> skuDetailsList) {
if(responseCode==BillingClient.BillingResponse.OK)
{
loadProductToRecyclerView(skuDetailsList);
}
else{
Toast.makeText(Donate.this, "Cannot query product", Toast.LENGTH_SHORT).show();
}
}
});
}
else
{
Toast.makeText(Donate.this, "Not ready", Toast.LENGTH_SHORT).show();
}
}
});
#Override
public void onPurchasesUpdated(int responseCode, #Nullable List<Purchase> purchases) {
if(purchases!=null){
Toast.makeText(this, "Purchased"+purchases.size(), Toast.LENGTH_SHORT).show();
}
}
That's by design and cannot be changed, in-app managed products can only be purchased once.
If you want the user who has paid more to have more features enabled, you will have to create as many in-app managed products as levels exist.
If it is a game in which, for example, the user is consuming items then when he no longer has any, you consume the in-app product so he can buy it again.
Or you can also consume the product immediately after the purchase and keep track of how many he has purchased through your own means, an own server or perhaps through firebase, but this already means that you will have to implement a user authentication system for your app.
Consume a purchase:
ConsumeResponseListener consumeListener = new ConsumeResponseListener() {
#Override
public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
}
};
String token = purchase.getPurchaseToken();
ConsumeParams consumeParams = ConsumeParams.newBuilder().setPurchaseToken(token).build();
billingClient.consumeAsync(consumeParams, consumeListener);
I am trying to create a restore purchase system. I want, the user can reach its bought products whichever device he/she logged in. So I use "queryPurchaseHistoryAsync()" method when app launches. My problem starts here.
With new implementation of Google, On contrary to the documentation, queryPurchaseHistoryAsync() parameters changed. Now it takes list of PurchaseHistoryRecord objects as parameter instead of list of Purchase objects.
Android studio can not resolve the method stated in the documentation. With new queryPurchaseHistoryAsync() I couldn't find anyway to check purchases state.( if it is purchased, canceled or pending). That I was able to do with Purchase object with "purchase.getPurchaseState()" method.
Documentation of queryPurchaseHistoryAsync()
billingClient.queryPurchaseHistoryAsync(SkuType.INAPP,
new PurchaseHistoryResponseListener() {
#Override
public void onPurchaseHistoryResponse(BillingResult billingResult,
List<Purchase> purchasesList) {
if (billingResult.getResponseCode() == BillingResponse.OK
&& purchasesList != null) {
for (Purchase purchase : purchasesList) {
// Process the result.
}
}
}
});
My implementation
implementation 'com.android.billingclient:billing:2.0.3'
queryPurchaseHistoryAsync() Method in my app
billingClient.queryPurchaseHistoryAsync(BillingClient.SkuType.INAPP,
new PurchaseHistoryResponseListener() {
#Override
public void onPurchaseHistoryResponse(BillingResult billingResult, List<PurchaseHistoryRecord> purchaseHistoryRecordList) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK
&& purchaseHistoryRecordList != null) {
for (PurchaseHistoryRecord purchaseHistoryRecord : purchaseHistoryRecordList) {
HandleOldGetting(purchaseHistoryRecord.getSku());
}
}
}
Release Note of Google(05-2019):
"To minimize confusion, queryPurchaseHistoryAsync() now returns a
PurchaseHistoryRecord object instead of a Purchase object. The PurchaseHistoryRecord object is the same as a Purchase object, except that it reflects only the values returned by queryPurchaseHistoryAsync() and does not contain the autoRenewing, orderId, and packageName fields. Note that nothing has changed with the returned data—queryPurchaseHistoryAsync() returns the same data as before."
But neither release note nor documentation state how to check Purchase State with PurchaseHistoryRecord.
Thank you for reading this, any help is appreciated.
So far, I have been using queryPurchases() to restore purchase automatically as it does not require any networking.
Google play app's cache related to account is updating for all devices. In many cases you won't need call to queryPurchaseHistoryAsync call for restoration.
As stated in #bospehre comment. It has drawback as it depends on the cache. So we still need to check purchases situations and restore them with network call.
For queryPurchaseHistory Async call, we can get the purchase sku and token. If you are using server to hold subscription datas as Google recommends. You can check this subscription's situations via your server.
Here is an example for restoring the latest subscription of the user.
billingManager.billingClient.queryPurchaseHistoryAsync(BillingClient.SkuType.SUBS) { billingResult, purchaseHistoryRecords ->
if (purchaseHistoryRecords != null) {
var activePurchaseRecord : PurchaseHistoryRecord? = null
if (purchaseHistoryRecords.size > 0) {
// Get the latest subscription. It may differ for developer needs.
for (purchaseHistoryRecord in purchaseHistoryRecords) {
Log.d(billingLogs, "Purchase History Record : $purchaseHistoryRecord")
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
if (subSkuListHelper.getSkuList().contains(purchaseHistoryRecord.sku)
) {
if (activePurchaseRecord == null) {
activePurchaseRecord = purchaseHistoryRecord
} else {
if (purchaseHistoryRecord.purchaseTime > activePurchaseRecord.purchaseTime) {
activePurchaseRecord = purchaseHistoryRecord
}
}
}
}
}
Toast.makeText(
this,
"Subscription Purchases found, Checking validity...",
Toast.LENGTH_SHORT
).show()
// Make a network call with sku and purchaseToken to get subscription info
//Subscription Data Fetch is a class that handling the networking
activePurchaseRecord?.let { SubscriptionDataFetch(
this,
billingManager.billingClient
)
.executeNetWorkCall(
getString(R.string.ubscription_check_endpoint),
it.sku,
it.purchaseToken
)
}
}
else {
Log.d(billingLogs, "Purchase History Record not found size 0") }
}
else {
Toast.makeText(
this,
"Purchase not found",
Toast.LENGTH_SHORT
).show()
Log.d(billingLogs, "Purchase History Record not found null")
}
}
I need some input here on how connection is made and querying the sku details. I'm working on the tutorial and copying the in app billing logic over to my app.
https://codelabs.developers.google.com/codelabs/play-billing-codelab
I followed the tutorial without any issues. The issue lies in how the connection is made and then querying the sku details.
When I made an instance of BillingManager class, it'll attempt to make a connection -
public BillingManager(Activity activity) {
mActivity = activity;
mBillingClient = BillingClient.newBuilder(mActivity).setListener(this).build();
mBillingClient.startConnection(new BillingClientStateListener() {
#Override
public void onBillingSetupFinished(#BillingClient.BillingResponse int billingResponse) {
if (billingResponse == BillingClient.BillingResponse.OK) {
Log.i(TAG, "onBillingSetupFinished() response: " + billingResponse);
} else {
Log.w(TAG, "onBillingSetupFinished() error code: " + billingResponse);
}
}
#Override
public void onBillingServiceDisconnected() {
Log.w(TAG, "onBillingServiceDisconnected()");
}
});
}
Then, I will be making async query to get the sku details -
private void handleManagerAndUiReady() {
// Start querying for SKUs
List<String> inAppSkus = mBillingProvider.getBillingManager()
.getSkus(SkuType.INAPP);
mBillingProvider.getBillingManager().querySkuDetailsAsync(SkuType.INAPP,
inAppSkus,
new SkuDetailsResponseListener() {
#Override
public void onSkuDetailsResponse(int responseCode,
List<SkuDetails> skuDetailsList) {
if (responseCode == BillingResponse.OK
&& skuDetailsList != null) {
for (SkuDetails details : skuDetailsList) {
Log.w(TAG, "Got a SKU: " + details);
}
}
}
});
// Show the UI
displayAnErrorIfNeeded();
}
Then I got the listener getting an error yet the connection is made without any issues.
D/StoreListFragment: onCreate
I/StoreListFragment: SkuDetailsResponseListener response code: -1
D/StoreListFragment: onViewCreated
I/BillingManager: onBillingSetupFinished() response: 0
So I had to figure out for some time and gave up to check the basics of Play Billing Library -
https://medium.com/exploring-android/exploring-the-play-billing-library-for-android-55321f282929
That is where I found the solution, I just put the query in the connection where it is successfully connected. I realized the play billing library doesn't check the connection BEFORE it goes querying the sku details or am I doing wrong somewhere since the tutorial is working fine?
private void createBillingClient() {
mBillingClient = BillingClient.newBuilder(getActivity()).setListener(this).build();
mBillingClient.startConnection(new BillingClientStateListener() {
#Override
public void onBillingSetupFinished(int billingResponse) {
if (billingResponse == BillingClient.BillingResponse.OK) {
Log.i(TAG, "onBillingSetupFinished() response: " + billingResponse);
//setting up a listener for the queries
SkuDetailsResponseListener responseListener = new SkuDetailsResponseListener() {
#Override
public void onSkuDetailsResponse(int responseCode,
List<SkuDetails> skuDetailsList) {
Log.i(TAG, "response code: " + responseCode);
}
};
List<String> skuList = Arrays.asList("sku_01", "sku_02");
SkuDetailsParams skuDetailsParams = SkuDetailsParams.newBuilder()
.setSkusList(skuList).setType(BillingClient.SkuType.SUBS).build();
mBillingClient.querySkuDetailsAsync(skuDetailsParams, responseListener);
} else {
Log.w(TAG, "onBillingSetupFinished() error code: " + billingResponse);
}
}
#Override
public void onBillingServiceDisconnected() {
Log.w(TAG, "onBillingServiceDisconnected()");
}
});
}
I tried this logic to check if the connection is ready then execute the runnable like as trivial drive - citing this url. It looks like the logic doesn't check if the billing connection is pending....
https://github.com/zumrywahid/in_app_example
Is Billing Client connected? : false
Client is already in the process of connecting to billing service.
onBillingSetupFinished() error code: 5
BillingManager constructor is already starting connection and if you initialize the manager and immediately call any method that passes runnable to executeServiceRequest(Runnable runnable) will also try to start connection at the same time. You can disable startServiceConnection() in the constructor since connection status is always checked in executeServiceRequest() and starts connection if needed.
I solved this issue in the following way:
I created a helper class that manages the billing and added listeners to it, so I can do things only when other actions are ready.
For example, I tried to show a list of in-app purchases in the onCreate of the activity, but the list was always empty. That was because the result of querySkuDetailsAsync was received after the code that displayed the list was executed.
I created a listener and I only display the list after the result is received.
interface BillingResponseIsAvailableListener {
void onBillingResponseIsAvailable(List<SkuDetails> theList);
}
public class BillingTools {
List<String> skuList = new ArrayList<>();
private List<BillingResponseIsAvailableListener> listeners = new ArrayList<>();
private BillingClient billingClient;
private Context context;
public BillingTools(Context context) {
this.context = context;
createSKUList(); //method to create the list of sku's for your app
}
public void addListener(BillingResponseIsAvailableListener toAdd) {
listeners.add(toAdd);
}
and where you query the sku list, just add the listener when the list is ready:
billingClient.querySkuDetailsAsync(params.build(), (billingResult2, skuDetailsList) -> {
if (billingResult2.getResponseCode() == BillingClient.BillingResponseCode.OK && skuDetailsList != null) {
for (BillingResponseIsAvailableListener bL : listeners) {
bL.onBillingResponseIsAvailable(skuDetailsList);
}
}
});
The above code is just an example, I recommend you create your own listeners for whatever you need to do. It is part of a method called queryOptionsList();
In the calling activity, just make the listener do whatever you need to do with the list:
BillingTools takeTheirMoney = new BillingTools(this);
takeTheirMoney.startBillingClient();
takeTheirMoney.addListener(new BillingResponseIsAvailableListener() {
#Override
public void onBillingResponseIsAvailable(List<SkuDetails> theList) {
showPurchaseOptions(findViewById(R.id.purchases_container), theList, takeTheirMoney);
}
});
takeTheirMoney.queryOptionsList();
If you are having problems with the connection not being ready, just add another listener to monitor when the connection is successful, and in that listener place additional actions (which can have their own listeners as well).
QueryInventoryFinishedListener of IabHelper has not returned the expired subscription items.
On the other hand, PurchaseHistoryResponseListener of Google Play Billing Library seems to receive all purchased items, which is including expired items.
On Google Play Billing Library, we have to check the purchased date of PurchaseHistoryResponseListener and each expiration date of items?
queryPurchases vs queryPurchaseHistoryAsync
Generally, we should use queryPurchases(String skuType), which does not returns expired items. queryPurchaseHistoryAsync returns enabled and disabled items, as you see the documentation like following.
queryPurchases
Get purchases details for all the items bought within your app. This method uses a cache of Google Play Store app without initiating a network request.
queryPurchaseHistoryAsync
Returns the most recent purchase made by the user for each SKU, even if that purchase is expired, canceled, or consumed.
About queryPurchaseHistoryAsync
I could not image the use case for queryPurchaseHistoryAsync. If we need to use queryPurchaseHistoryAsync, we need the implementation to check if it is expired or not.
private PurchaseHistoryResponseListener listener = new PurchaseHistoryResponseListener() {
#Override
public void onPurchaseHistoryResponse(int responseCode, List<Purchase> purchasesList) {
for (Purchase purchase : purchasesList) {
if (purchase.getSku().equals("sku_id")) {
long purchaseTime = purchase.getPurchaseTime();
// boolean expired = purchaseTime + period < now
}
}
}
};
Purchase object does not have the information of period, so the above period must be acquired from BillingClient.querySkuDetailsAsync or be hard-coded. The following is sample implementation to use querySkuDetailsAsync.
List<String> skuList = new ArrayList<>();
skuList.add("sku_id");
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
params.setSkusList(skuList).setType(BillingClient.SkuType.SUBS);
billingClient.querySkuDetailsAsync(params.build(), new SkuDetailsResponseListener() {
#Override
public void onSkuDetailsResponse(int responseCode, List<SkuDetails> skuDetailsList) {
if (skuDetailsList == null) {
return;
}
for (SkuDetails skuDetail : skuDetailsList) {
if (skuDetail.getSku().equals("sku_id")) {
String period = skuDetail.getSubscriptionPeriod();
}
}
}
});
I'm making an Android application that will include a subscription using in-app billing from Google (https://developer.android.com/google/play/billing/index.html).
The aim is to let user to have 1 subscription per device.
I know that Google limit subscription for 1 google mail account but not for 1 device, that's why I made my own restriction using a server with a database.
So I made a pool of 10 subscriptions in the developer console product list and I want that when a device subscribe for the 1st subscription, a second device (using the same google account) will subscribe for the next subscription...
But when I want the second device chose automatically the next subscription not even bought on the account, it is saying to me "Product already own". The problem is that the current inventory is not refreshed.
I'm using IabHelper and here is the part of code where I'm trying to buy the next subscription available.
public void initIab() throws IabHelper.IabAsyncInProgressException {
iabHelper = new IabHelper(this, AppConfig.APPLICATION_KEY);
iabHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() {
public void onIabSetupFinished(IabResult result) throws IabHelper.IabAsyncInProgressException {
if (result.isSuccess()) {
iabHelper.queryInventoryAsync(iabInventoryListener());
billingServiceReady = true;
}
}
});
}
private IabHelper.QueryInventoryFinishedListener iabInventoryListener() {
return new IabHelper.QueryInventoryFinishedListener() {
public void onQueryInventoryFinished(IabResult result, Inventory inventory) {
if (iabHelper == null) {
return;
}
if (!result.isSuccess()) {
return;
}
String[] PREMIUM_KEYS = {"premium1","premium2","premium3","premium4","premium5","premium6","premium7","premium8","premium9","premium10"};
Purchase premiumPurchase = null;
String premiumKey;
boolean payload = false;
for(String key : PREMIUM_KEYS) {
if (inventory.hasPurchase(key)) {
premiumPurchase = inventory.getPurchase(key);
premiumKey = key;
payload = verifyDeveloperPayload(hasPurchase,premiumKey);
if(payload)
break;
}
}
session.setPremium(achatPremium != null && payload);
}
};
}
Please could you help me to find a solution ?
Sorry for my bad english.
Thank you.