Library : https://github.com/anjlab/android-inapp-billing-v3
I am using the Android In-App Billing v3 library and would like to sell one product more than once. If the subscription is purchased, I provide 1 usage right to the user. If the user wishes, he should be able to create his second subscription and buy the second right.
bp.consumePurchase (getString (R.string.trackmonth)); this code did not work.
#Override
public void onBillingInitialized() {
boolean controlnumber = bp.loadOwnedPurchasesFromGoogle();
if(controlnumber) {
TransactionDetails subscriptionTransactionDetails = bp.getSubscriptionTransactionDetails(getString(R.string.trackmonth));
bp.consumePurchase(getString(R.string.trackmonth));
if (subscriptionTransactionDetails != null) {
Log.d("TAG", "onBillingInitialized: active");
} else {
Log.d("TAG", "onBillingInitialized: not");
}
}
#Override
public void onPurchaseHistoryRestored() {
Log.d("TAG", "onPurchaseHistoryRestored: ");
bp.consumePurchase(getString(R.string.trackmonth));
}
Subscriptions are tied to a period of time and a renewal, consuming them makes little sense.
You can't consume subscriptions, use an in-app product instead
Related
I refresh to android billing version 4 and 2 things are not working anymore.
First I have this:
else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED) {
Purchase.PurchasesResult queryAlreadyPurchasesResult = billingClient.queryPurchases(INAPP); // deprecated
List<Purchase> alreadyPurchases = queryAlreadyPurchasesResult.getPurchasesList();
if(alreadyPurchases!=null){
handlePurchases(alreadyPurchases);
}
}
queryPurchases is deprecated.
Second I have this:
void handlePurchases(List<Purchase> purchases) {
for(Purchase purchase:purchases) {
//if item is purchased
if (PRODUCT_ID.equals(purchase.getSku()) && purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED)
{
if (!verifyValidSignature(purchase.getOriginalJson(), purchase.getSignature())) {
// Invalid purchase
// show error to user
Toast.makeText(getApplicationContext(), R.string.plus_error, Toast.LENGTH_SHORT).show();
return;
}
getSku() was working, but now it is mark as Cannot resolve method getSku() in Purchase
Any ideas how to solve this issues?
From docs:
Summary of changes
Added BillingClient.queryPurchasesAsync() to replace BillingClient.queryPurchases() which will be removed in a future release.
Added Purchase#getSkus() and PurchaseHistoryRecord#getSkus(). These replace Purchase#getSku and PurchaseHistoryRecord#getSku which have been removed.
But I don't know how to apply this new commands in my code above.
If I change getSku to getSkus my if if (PRODUCT_ID.equals(purchase.getSkus()) && purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) will say that it is always false. And I have no idea how to use queryPurchasesAsync(), need 2 params now.
Thanks.
As I mentioned earlier in a comment you are comparing a String to a List object, but as chitgoks said it is ArrayList<String> and not List<String> as i assumed. I'm not sure if you would ever get more than one sku-string (since you probably don't order multiple things at the same time?) but either look trough them all to be sure or take a chance and compare PRODUCT_ID with only purchase.getSkus().get(0).
The new async call for purchases seems to require only small changes.
Example of old way to do it:
Purchase.PurchasesResult result = billingClient.queryPurchases(BillingClient.SkuType.SUBS);
doSomethingWithPurchaseList(result.getPurchasesList());
And this would be the new way to do the same:
billingClient.queryPurchasesAsync(BillingClient.SkuType.SUBS, new PurchasesResponseListener() {
#Override
public void onQueryPurchasesResponse(#NonNull BillingResult billingResult, #NonNull List<Purchase> list) {
doSomethingWithPurchaseList(list);
}
});
getSkus returns an ArrayList<String>. Please use contains as below.
purchase.getSkus().contains(YOUR_PRODUCT_ID.toLowerCase())
Posting this a year in. As with billing 4.0.0, the documentation 'Integrate the Google Play Billing Library into your app' as of billing 5.0.0 is buggy and incomplete, although possibly not as bad as a year ago. Now we are dealing with ProductDetails instead of SkuDetails objects. Also note the following corrections in documentation:
QueryProductDetailsParams queryProductDetailsParams =
QueryProductDetailsParams.newBuilder()
.setProductList(
ImmutableList.of(
should be:
QueryProductDetailsParams queryProductDetailsParams =
QueryProductDetailsParams.newBuilder()
.setProductList(
ImmutableList.from,(//'from' instead of 'of'
...
BillingFlowParams billingFlowParams =
BillingFlowParams.newBuilder()
.setProductDetailsParamsList(
ImmuableList.of(
should be:
BillingFlowParams billingFlowParams =
BillingFlowParams.newBuilder()
.setProductDetailsParamsList(
ImmutableList.from(//'ImmutableList.from' instead of 'ImmuableList.of'
...
billingClient.queryProductDetailsAsync(
queryProductDetailsParams,
new ProductDetailsResponseListener() {
public void onProductDetailsResponse(BillingResult billingResult,
List<ProductDetails> productDetailsList) () {
should be:
billingClient.queryProductDetailsAsync(
queryProductDetailsParams,
new ProductDetailsResponseListener() {
public void onProductDetailsResponse(BillingResult billingResult,
List<ProductDetails> productDetailsList) {//no extra parens
...
//Incomplete
billingClient.queryPurchasesAsync(
QueryPurchasesParams.newBuilder()
.setProductType(ProductType.SUBS)
.build(),
/* purchaseResponseListener= */ this
);
// PurchaseResponseListener implementation.
public void onQueryPurchasesResponse(BillingResult billingResult, List<Purchase> purchases) {
// check BillingResult
// process returned purchase list, e.g. display the plans user owns
}
The most disappointing part of documentation IMHO. It just gives clues and the 'this' is missleading, you will get an error with the suggestion to cast purchaseResponseListener to it. An actual implementation would be:
billingClient.queryPurchasesAsync(
QueryPurchasesParams.newBuilder()
.setProductType(BillingClient.ProductType.INAPP)//or SUBS
.build(),
new PurchasesResponseListener() {
#Override
public void onQueryPurchasesResponse(BillingResult billingResult, List<Purchase> purchases) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK&&purchases != null) {
for (Purchase purchase : purchases) {
handlePurchase(purchase);
}
}
}
}
);
In your code, 'purchase.getSku' won't be recognized as the method was 'purchase.getSkus()'. This is depreciated now anyways and you would use the following to pull the product id (sku) off. Likely you will have just one product for a puchase object, although users can now buy multiple products with a single purchase:
purchase.getProducts().get(0)
So you all know new billing library has new feature
every thing will be in background thread so do not do change anything on main UI during acknowledgement of purchase and restoring purchase.
if you are giving consumable purchase then user can now buy same sku in more quantity in one purchase so write logic accordingly. use getQuantity() function.
to restore non consumable.
billingClient.queryPurchasesAsync(
BillingClient.SkuType.INAPP,
new PurchasesResponseListener() {
#Override
public void onQueryPurchasesResponse(#NonNull BillingResult billingResult, #NonNull List < Purchase > myPurchases) {
if (!myPurchases.isEmpty()) {
for (Object p: myPurchases) {
final Purchase purchase = (Purchase) p;
if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED && purchase.getSkus.contains("sku here") {
handlePurchase(purchase);
}
}
});
}
}
)
I'm using Retrofit, RxJava and MVVM pattern in my app.
I have API that retrieve me a list of movies.
In the API request I must to say what page I want to load ( The API retrieve me 1 page (20 items) for each request).
At the start of the app I want to load 3 pages.
How to I do that?
That what i have now:
Repository:
public MutableLiveData<List<MoviesResult>> getMoviesResultsMutableLiveData(String sort_by, int page, String with_genres, String with_cast) {
MoviesService moviesService = RetrofitInstance.getMoviesService();
mDisposable = moviesService.getMovies(
mApplication.getResources().getString(R.string.api_key),
sort_by,
UserSettings.getInstance().includeAdults(),
true, page, with_genres, with_cast)
.retryWhen(throwable ->
throwable.delay(5, TimeUnit.SECONDS))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<MoviesResponse>() {
#Override
public void accept(MoviesResponse moviesResponse) throws Throwable {
mMoviesResultsMutableLiveData.setValue(moviesResponse.getMoviesResults());
mDisposable.dispose();
}
}, new Consumer<Throwable>() {
#Override
public void accept(Throwable throwable) throws Throwable {
CustomToast customToast = new CustomToast(mApplication.getApplicationContext());
customToast.makeCustomText(throwable.getMessage());
mDisposable.dispose();
}
});
return mMoviesResultsMutableLiveData;
}
ViewModel:
public MutableLiveData<List<MoviesResult>> getMoviesResultMutableLiveData(String sort_by, int page, String with_genres, String with_cast) {
return mMoviesRepository.getMoviesResultsMutableLiveData(sort_by, page, with_genres, with_cast);
}
And MainActivity:
mMoviesViewModel.getMoviesResultMutableLiveData(getString(R.string.sort_by_popularity), 1, null, null)
.observe(this, new Observer<List<MoviesResult>>() {
#Override
public void onChanged(List<MoviesResult> moviesResults) {
Log.d(TAG, "onChanged: "+moviesResults.size());
}
});
I've tried to make a for loop in main activity when "i" equals to pages I want to load, and then set the i to the page in getMovies method - but it's not worked for me.
for (int i = 0; i < 3; i++) {
mMoviesViewModel.getMoviesResultMutableLiveData(getString(R.string.sort_by_popularity), i, null, null)
.observe(this, new Observer<List<MoviesResult>>() {
#Override
public void onChanged(List<MoviesResult> moviesResults) {
Log.d(TAG, "onChanged: "+moviesResults.size());
}
});
}
In this case i receive only 1 page .
Link to add Paging without using paging library Link
It's not a good idea to add logic inside your View (Activity/Fragment), you are using a for loop inside View which is not ideal. So move this into your ViewModel or UseCase.
RxJava has an operator named concatWith which concats multiple observables together. thus when your Activity/Fragment lunches you can initiate three API calls and contact them together and return the three as a single model to your observer.
By the way, Using Android Paging from Android Jetpack is a better solution for what you what to achieve.
onPurchasesUpdated unable to consumeAsync and billingResult result error code 6.
I have tried moving consumeAsync to queryPurchases and queryPurchaseHistoryAsync, and the purchase will only be consume in queryPurchaseHistoryAsync and not queryPurchases
billingClient = BillingClient.newBuilder(this).setListener(new PurchasesUpdatedListener() {
#Override
public void onPurchasesUpdated(BillingResult billingResult, #Nullable List<Purchase> purchases) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK
&& purchases != null) {
for (Purchase purchase : purchases) {
handlePurchase(purchase);
//The following consumeAsync not working, return error code 6
ConsumeParams consumeParams = ConsumeParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.setDeveloperPayload(purchase.getDeveloperPayload())
.build();
billingClient.consumeAsync(consumeParams, new ConsumeResponseListener() {
#Override
public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
//billingResult return code 6 here.
Log.i("TAG", String.valueOf(billingResult.getResponseCode()));
}
});
}
} else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED) {
// Handle an error caused by a user cancelling the purchase flow.
} else {
// Handle any other error codes.
}
}
}).enablePendingPurchases().build();
consumeAsync not working under queryPurchases():
Purchase.PurchasesResult purchasesResult = billingClient.queryPurchases(BillingClient.SkuType.INAPP);
for(Purchase purchase : purchasesResult.getPurchasesList()) {
handlePurchase(purchase);
ConsumeParams consumeParams = ConsumeParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.setDeveloperPayload(purchase.getDeveloperPayload())
.build();
billingClient.consumeAsync(consumeParams, new ConsumeResponseListener() {
#Override
public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
Log.i("TAG", String.valueOf(billingResult.getResponseCode()));
}
});
}
consumeAsync only works under the following code (I understand that the method is wrong and the item will be consumed over and over again and return code 8, but it is the only way that the item will be consumed):
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 purchase : purchaseHistoryRecordList) {
handlePurchaseHistoryRecord(purchase);
ConsumeParams consumeParams = ConsumeParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.setDeveloperPayload(purchase.getDeveloperPayload())
.build();
billingClient.consumeAsync(consumeParams, new ConsumeResponseListener() {
#Override
public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
Log.i("TAG", "item consumed");
}
});
}
}
}
});
Your code will consume every purchase as soon as it is made. In general, you would only want to consume a managed product if you want the buyer to be able to repurchase that product again. This might be the case if your item is expected to be consumable, such as additional coins in a game. Once the coins run out, you would consume the purchase, then the buyer could purchase more coins.
Another scenario where you might consume a purchase would be for testing, where you might want to purchase a product multiple times. You could set up a temporary button or other method to consume the purchase so that you can continue repurchasing while working out any bugs in your code.
A third scenario might be after you have issued a buyer a refund. After the refund, you might want to consume the purchase to give the buyer an option to purchase the product again.
These are the only three scenarios I can think of off hand where you would want to consume a managed product.
Edit
To YKW's comment:
YKW made a good point about purchasing additional coins that didn't initially occur to me... but either way, you can't consume the product a second time until you get a valid consumption response back from the first purchase consumption and then the product is purchased again. In this case, at a minimum you should first check to make sure the purchase is not PurchaseState.Pending. Then consume the product, not allowing the product to be purchased again until you get the onConsume response back. You will also want to check each time your app and/or activity starts and consume any purchased products as google confirms that occasionally a consumption will fail.
End Edit
UPDATE
Another possibility for receiving response code 6 is the developer payload string is empty. You can remove the .setDeveloperPayload(purchase.getDeveloperPayload()) line as specifying a developer payload is optional for ConsumeParams.
If you want to send a developer payload, make sure that purchase.getDeveloperPayload() is not returning an empty string.
END UPDATE
There is a good example of a method for consuming a purchase in the trivial drive app.
Trivial Drive V2 BillingManager
Here is a method I use for debugging based on the one found in the trivial drive V2 app.
void consumeAsync(final ConsumeParams consumeParams) {
// If we've already scheduled to consume this token - no action is needed (this could happen
// if you received the token when querying purchases inside onReceive() and later from
// onActivityResult()
if (paramsSetToConsume == null) {
paramsSetToConsume = new HashSet<>();
} else if (paramsSetToConsume.contains(consumeParams)) {
Log.i(TAG, "Params was already scheduled to be consumed - skipping...");
return;
}
paramsSetToConsume.add(consumeParams);
// Generating Consume Response listener
final ConsumeResponseListener onConsumeListener = new ConsumeResponseListener() {
#Override
public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
// If billing service was disconnected, we try to reconnect 1 time
// (feel free to introduce your retry policy here).
Log.d(TAG, "ConsumeResponseListener fired. sending to onConsumedFinished listener");
mBillingUpdatesListener.onConsumeFinished(consumeParams.getPurchaseToken(), billingResult.getResponseCode());
}
};
// Creating a runnable from the request to use it inside our connection retry policy below
Runnable consumeRequest = new Runnable() {
#Override
public void run() {
// Consume the purchase async
Log.d(TAG, "consumeParams being sent to billingClient to consume");
billingClient.consumeAsync(consumeParams, onConsumeListener);
}
};
executeServiceRequest(consumeRequest);
}
Then I set up a temporary button, create my ConsumeParams and call this method in the onClick listener.
paramsSetToConsume is a member variable
private Set<ConsumeParams> paramsSetToConsume;
This question already has answers here:
How to save users score in firebase and retrieve it in real-time in Android studio
(3 answers)
Closed 4 years ago.
I'm having a problem when I try to star a movie which id doesn't exist in my likes firebase node. Here's my code:
private void onStarClicked(long releaseId, final String uid) {
final DatabaseReference postRef = ((GamePageActivity)getActivity()).mDatabaseRef.child("likes").child(mRegion).child(String.valueOf(releaseId));
postRef.runTransaction(new Transaction.Handler() {
#Override
public Transaction.Result doTransaction(MutableData mutableData) {
_Post p = mutableData.getValue(_Post.class);
if (p == null) {
Log.d(TAG, "Transaction success");
return Transaction.success(mutableData);
}
if (p.stars.containsKey(uid)) {
// Unstar the post and remove self from stars
p.starCount = p.starCount - 1;
p.stars.remove(uid);
} else {
// Star the post and add self to stars
p.starCount = p.starCount + 1;
p.stars.put(uid, true);
}
// Set value and report transaction success
mutableData.setValue(p);
return Transaction.success(mutableData);
}
#Override
public void onComplete(DatabaseError databaseError, boolean b,
DataSnapshot dataSnapshot) {
// Transaction completed
Log.d(TAG, "postTransaction:onComplete:" + databaseError);
}
});
}
The problem is if my id "releaseId" doesn't exist in the database, the star won't get added in, I thought my code was supposed to first add the "releaseId" if it doesn't exist?
It seems a bit complicated to try to add a movie inside your likes path during a ‘like’ transaction. Ideally, it would already be there. So it will be easier to separate adding the movie to the likes path, and after that use a Cloud Function onCreate or onUpdate.
I assume you have your movie data somewhere else in your database. Let’s call it the ‘movies’ path for now. One idea to try is to have a Cloud Function that watches your ‘movies’ path, and when a movie is added, the function will add that movie data to your ‘likes’ path. That way, once a user tries to like a movie, its information is already in the correct spot in the database. Fixing the problem of the non-existent 'releaseId'.
Firebase Realtime Database Triggers
If you don’t want to use a Cloud Function, you can consider writing to both the ‘movies’ path and ‘likes’ path directly from the client when the movie is added, to accomplish the same thing.
I have an application that offers some offers for some stuff. The idea is the offer is added by me on another application I created and then be shown on users devices all at the same time.
I need only one user to be able to take the offer so when the first user clicks I do call removeValue() on the offer ref. The offer is correctly deleted from the database and from the other users recyclerview.
The problem is when 2 clicks happens in the same time the offer is deleted but onChildRemoved() doesn't have the time to be called so both users now have the same offer!
Is there any other idea how to make this operation more precise and time aware?
UPDATE as suggested from svi.data i tried this piece of code on user click but still the same problem occur.
offerUnAnsweredRef.addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(#NonNull DataSnapshot dataSnapshot) {
boolean stillThere = false;
for (DataSnapshot offerSnap : dataSnapshot.getChildren()) {
if (offerSnap.getKey().equals(requestedOffer.getCurrentNodeKey())) {
stillThere = true;
}
}
if (stillThere) {
Timber.d("We have it " + requestedOffer.getEmployeeKey());
Toast.makeText(getContext(), "Welcome Dear ", Toast.LENGTH_SHORT).show();
offerUnAnsweredRef.child(requestedOffer.getCurrentNodeKey()).removeValue();
} else {
Toast.makeText(getContext(), "Go Away Bear", Toast.LENGTH_SHORT).show();
}
}
#Override
public void onCancelled(#NonNull DatabaseError databaseError) {
}
});
UPDATE 2
Actually the solution is built on top of svi.data answer with some modifications so i wanted to share the working code to help if any one ever come cross similar situation
offerUnAnsweredRef.child(requestedOffer.getCurrentNodeKey()).runTransaction(new Transaction.Handler() {
#NonNull
#Override
public Transaction.Result doTransaction(#NonNull MutableData mutableData) {
RequestedOffer o = mutableData.getValue(RequestedOffer.class);
if (o == null) {
return Transaction.abort();
}
if (o.getEmployeeKey() == null) {
o.setEmployeeKey(employee.getUid());
mutableData.setValue(o);
return Transaction.success(mutableData);
} else {
return Transaction.success(mutableData);
}
}
#Override
public void onComplete(DatabaseError databaseError, boolean b,
DataSnapshot dataSnapshot) {
// check if the transaction completed successfully
// or if it failed
RequestedOffer o = dataSnapshot.getValue(RequestedOffer.class);
if (o.getEmployeeKey() == employee.getUid()) {
getActivity().runOnUiThread(() -> Toast.makeText(getActivity(), "Hello", Toast.LENGTH_SHORT).show());
DatabaseReference databaseReference = FirebaseFactory.getDatabase()
} else {
getActivity().runOnUiThread(() -> Toast.makeText(getActivity(), "NO", Toast.LENGTH_SHORT).show());
}
}
because as documentation says
public abstract Transaction.Result doTransaction (MutableData currentData)
This method will be called, possibly multiple times, with the current data at this location. It is responsible for inspecting that data and returning a Transaction.Result specifying either the desired new data at the location or that the transaction should be aborted.
So i added the code to check in onComplete to ensure that it called only once.
From what I understand:
1) You have a specific app for adding the offers (by you).
2) You have another app for reading the offers (by users).
3) If this is the case then both apps use the same project.
4) When a user clicks an offer, he/she will get the offer, then you will delete the offer from the database.
5) Now when 2 users click the same offer there is no time for the offer to be removed from the other user's list, so they end up with the same offer.
Now it seems that you don't want users to get same offers, and the problem really is a timing issue.
Possible solution:
1) When ever a user clicks an offer, you run a ValueEventListener() to the offers node in the database and check if the offer exist.
2) If the offer exists give him/her the offer and delete it.
3) Now when 2 users click the same offer, the ValueEventListener that I talked about will provide you with some time before reacting.
4) So users shouldn't end up with same offers.
Hope it solves your problem.
UPDATE:
As this is is a race condition between users, its time to talk about transactions. Firebase provides a nice way to directly read and write concurrent updates to the same node (which is your case).
I want your database to be like this:
Offers
|
|------offer_id_1
|
|-----taken:false
|-----......
|-----......
|
|-------offer_id_2
|
|------taken:false
|------......
|------......
Let me explain the above structure, each offer you post from the other application will have a flag by default called taken and it should have by default a value of false.
Now as you see above offer_id_1 and offer_id_2 are the push id or the random id given for the offer (when a user click on an offer you must get a reference of this key....I assume you know how to do this).
Before we start ofcourse you should have a model class for your posts we will call it Offer its just a class:
public class Offer{
public boolean taken;
......
......
}
The below function is what you will call after someone clicked an offer (we will use a transaction):
public void RunTransactionFor(String offer_id){
//first refer to your offers
DatabaseReference offers_ref = FirebaseDatabase.getInstance().getReference().child("offers").child(offer_id);
//run a transaction (a transaction is fast it reads and writes directly)
offer_ref.runTransaction(new Transaction.Handler() {
#Override
public Transaction.Result doTransaction(MutableData mutableData) {
//this is a ref to the offer class
Offer offer = mutableData.getValue(Offer.class);
if (offer == null) {
return Transaction.success(mutableData);
}
if(offer.taken == false){
//take the offer
offer.taken = true;
//show a message
Toast.makeText(context, "you took the offer",...).show();
//now you can remove the offer
offers_ref.setValue(null);//or delete it your way
}else{
//too late the offer is taken
Toast.makeText(context, "too late the offer is gone",...).show();
//do nothing
}
// Set value and report transaction success
mutableData.setValue(offer);
return Transaction.success(mutableData);
}
#Override
public void onComplete(DatabaseError databaseError, boolean b,
DataSnapshot dataSnapshot) {
// Transaction completed
}
});
}
Now when a user clicks an offer in the list store the id of the offer and pass it to the above function like this
//after user clicks
String offer_id = .......
//run transaction
RunTransactionFor(offer_id);
Note: Transactions only work online they can't work offline.