I'm trying to implement In-app messaging to display a snackbar if a subscription has had it's payment declined.
Following the documentation here and adding billingClient.showInAppMessages doesn't seem to work. I subscribe using the Test card, always approves and change it to Test card, always declines and wait for the payment to be put in grace period, but the snackbar from the documentation does not show up even after restarting the application.
Expected result after payment has been declined and app was restarted:
In-app messaging works as I can send messages via firebase, but I am unsure if I'm missing something obvious here?
Implementation:
(This is called on app start)
// onCreate
billingClient = createBillingClient()
setupInAppMessaging(activity)
if (!billingClient.isReady) {
logD { "BillingClient: Start connection..." }
billingClient.startConnection(this)
}
fun createBillingClient() = BillingClient.newBuilder(context)
.setListener(this)
.enablePendingPurchases()
.build()
fun setupInAppMessaging(activity: Activity) {
val inAppMessageParams = InAppMessageParams.newBuilder()
.addInAppMessageCategoryToShow(InAppMessageParams.InAppMessageCategoryId.TRANSACTIONAL)
.build()
billingClient.showInAppMessages(activity, inAppMessageParams) { inAppMessageResult ->
if (inAppMessageResult.responseCode == InAppMessageResult.InAppMessageResponseCode.NO_ACTION_NEEDED) {
// The flow has finished and there is no action needed from developers.
logD { "SUBTEST: NO_ACTION_NEEDED"}
} else if (inAppMessageResult.responseCode == InAppMessageResult.InAppMessageResponseCode.SUBSCRIPTION_STATUS_UPDATED) {
logD { "SUBTEST: SUBSCRIPTION_STATUS_UPDATED"}
// The subscription status changed. For example, a subscription
// has been recovered from a suspend state. Developers should
// expect the purchase token to be returned with this response
// code and use the purchase token with the Google Play
// Developer API.
}
}
}
Has it worked at all? I believe these messages are only shown once per day so if you already saw it once, you will have to wait another 24 hours to see it again if your payment method is still failing.
Related
I am trying to implement Sign in with Apple using Firebase Authentication. I am following the firebase/quickstart-android sample.
My sign-in fragment overrides onStart() to check for any pending results:
override fun onStart() {
super.onStart()
val pending = auth.pendingAuthResult
pending?.addOnSuccessListener { authResult ->
Timber.d("Successful login, pending")
}?.addOnFailureListener { e ->
Timber.d("Failed login, pending")
}
}
And a button that initiates the sign-in flow:
btnApple.onClick {
viewModel.appleLogin(requireActivity())
}
The viewModel calls the following method from a repository:
// Initiate sign-in flow only if there are no pending results
if (auth.pendingAuthResult != null) {
return
}
val scopes = listOf("email", "name")
val provider = OAuthProvider.newBuilder("apple.com", auth)
.setScopes(scopes)
.build()
auth.startActivityForSignInWithProvider(activity, provider)
.addOnSuccessListener { authResult ->
Timber.d("Successful login, normal")
}
.addOnFailureListener { e ->
Timber.e(e, "Failed login, normal")
}
The official manual states:
Signing in with this method puts your Activity in the background, which means that it can be reclaimed by the system during the sign in flow.
So I started testing the pending result by terminating the app in Android Studio while completing the sign-in flow in Chrome. Once I returned back to the app, the onStart() was called, but the pendingAuthResult was always null.
To make it more interesting, when I restart the app, I am logged in. Then if I log out and enter the sign-in fragment again, there is a pending result now and I receive Successful login, pending. On top of that, the pending result does not disappear. If I leave the sign-in fragment and go back, the pending result is still there and I receive yet another Successful login, pending.
I even tested the firebase/quickstart-android sample itself and it has exactly the same issue.
What could be the possible cause of this issue? I am using firebase-auth:19.2.0.
if you are using the firebase for signing with apple and if it's handled manually then you should use a string decoding mechanism, Please find the firebase guide for the same and hope it'll works. let me know if any issue!
Firebase guild signing with apple
The Google Play Billing library documentation on acknowledging purchases states that:
you must acknowledge all purchases that have a SUCCESS state received through the Google Play Billing Library as soon as possible after granting entitlement to the user.
So your app should first give the user what he bought, and then acknowledge the purchase. This is also what their example code implies:
fun handlePurchase() {
if (purchase.purchaseState === PurchaseState.PURCHASED) {
// Grant entitlement to the user.
...
// Acknowledge the purchase if it hasn't already been acknowledged.
if (!purchase.isAcknowledged) {
val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.purchaseToken)
.build()
client.acknowledgePurchase(acknowledgePurchaseParams, acknowledgePurchaseResponseListener)
}
}
}
I am currently developing an Android app where I give users access to their bought content after the acknoweldgement returned with an OK status (so the other way around):
private fun acknowledgePurchase(purchase: Purchase) {
val params = AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.purchaseToken)
.build()
billingClient.acknowledgePurchase(params) { billingResult ->
when (billingResult.responseCode) {
BillingResponseCode.OK -> loadPurchase(purchase.sku)
else -> { ... }
}
}
}
because the documentation states that:
... you must acknowledge all purchases within three days. Failure to properly acknowledge purchases results in those purchases being refunded.
If this happens, and a user gets refunded, the user's entitlment to the product should also be withdrawn. But in my case, making a purchase means loading data, storing stuff in the database, and so on, and I do not have, nor do I want to have, code in place to revert this. Therefore I provide the user with the product only after a successful acknowledge.
Hence my question: is it okay to wait with granting entitlement to an in-app purchase until after the purchase has been acknowledged? Or is there some catch I am missing here? I assume that the documentation specifies this order for a reason, but they do not elaborate on it.
After some more research I noticed that the Trivial Drive sample app takes the exact same approach as I do. The following is a snippet of their code:
private fun acknowledgeNonConsumablePurchasesAsync(nonConsumables: List<Purchase>) {
nonConsumables.forEach { purchase ->
val params = AcknowledgePurchaseParams.newBuilder().setPurchaseToken(purchase
.purchaseToken).build()
playStoreBillingClient.acknowledgePurchase(params) { billingResult ->
when (billingResult.responseCode) {
BillingClient.BillingResponseCode.OK -> {
disburseNonConsumableEntitlement(purchase)
}
else -> Log.d(LOG_TAG, "acknowledgeNonConsumablePurchasesAsync response is ${billingResult.debugMessage}")
}
}
}
}
showing that entitilement is granted in the acknowledge callback function, if the response was OK. Exactly the same as the code in my question.
So I guess the answer to the question is: yes, it is okay to grant entitlement after successful acknowledgement.
I'm having trouble figuring out how to detect when a refund has been issued for a managed (uncomsumable) in-app product in Android using com.android.billingclient:billing:2.0.3. The problem seems fairly deep though maybe I'm making it more complicated than it ought to be.
To begin, I've made a test purchase which has been acknowledged AND refunded:
Looking at the logs of my app I see the following:
D/BillingManager: Got a verified purchase: Purchase. Json: {"orderId":"GPA.3362-7185-5389-78416","packageName":"com.glan.input","productId":"pro","purchaseTime":1567672759460,"purchaseState":0,"purchaseToken":"pbkpcaadklleoecegjfjdpbl.AO-J1OwsR6WVaVZCCYOU6JyYN1r0qJsrwitIPZfhc3jX4yketRUwNzKqwMgYx0TgZ2GebEGbXDL0RlMyogwtSKSPsaHCJ4RA4MPlIGay-aM1-QhmnqwjXjQ","acknowledged":true}
I/BillingManager: purchase pbkpcaadklleoecegjfjdpbl.AO-J1OwsR6WVaVZCCYOU6JyYN1r0qJsrwitIPZfhc3jX4yketRUwNzKqwMgYx0TgZ2GebEGbXDL0RlMyogwtSKSPsaHCJ4RA4MPlIGay-aM1-QhmnqwjXjQ is in 1 state
There's something funny going on here:
We can see the order IDs match up between what's in the image and the detected purchase
The first log line is printing the purchase with Log.d(TAG, "Got a verified purchase: " + purchase); which is printing the underlying JSON which represents the purchase.
Note that "purchaseState":0
The second log line is issued with Log.i(TAG, "purchase " + purchase.getPurchaseToken() + " is in " + purchase.getPurchaseState() + " state");.
Note that here purchase.getPurchaseState() is resulting in a value of 1
If I look at the implementation of getPurchaseState in Android Studio I see the following:
public #PurchaseState int getPurchaseState() {
switch (mParsedJson.optInt("purchaseState", PurchaseState.PURCHASED)) {
case 4:
return PurchaseState.PENDING;
default:
return PurchaseState.PURCHASED;
}
}
Earlier in the file the PurchaseState interface is declared as:
#Retention(SOURCE)
public #interface PurchaseState {
// Purchase with unknown state.
int UNSPECIFIED_STATE = 0;
// Purchase is completed.
int PURCHASED = 1;
// Purchase is waiting for payment completion.
int PENDING = 2;
}
It seems like getPurchaseState never returns PurchaseState.UNSPECIFIED_STATE and only returns PENDING which the JSON comes with a value of 4. I've confirmed that a state of PENDING is correctly returned when the purchase is performed with a payment method that takes a while to approve.
I've found posts like In-App Billing v3 - Don't detect refund which suggest that Play Services are caching purchases but I'm not convinced that's causing this problem because if I modify my code betweens runs of my app to acknowledge/consume the purchase those get state changes get immediately reflected in the JSON of the purchase.
How am I supposed to detect a refunded managed product?
I have one purchase (SkuType.INAPP) in my application. I make a test purchase and then make a refund.
Problem:
purchase.getOriginalJson() // contains "purchaseState":0
purchase.getPurchaseState() // returns 1
Inside com.android.billingclient.api.Purchase:
public int getPurchaseState() {
switch(this.zzc.optInt("purchaseState", 1)) {
case 4:
return 2;
default:
return 1;
}
}
//...
public #interface PurchaseState {
int UNSPECIFIED_STATE = 0;
int PURCHASED = 1;
int PENDING = 2;
}
Hacky way to check purchaseState from original json:
purchase.getOriginalJson().contains(String.format("\"purchaseState\":%s", Purchase.PurchaseState.PURCHASED))
Unfortunately, this problem still exists!
More details here.
You can check if still purchase exits following
binding.btnRestore.setOnClickListener(v->{
Purchase.PurchasesResult purchasesResult = mBillingClient.queryPurchases(BillingClient.SkuType.INAPP);
for ( Purchase purchase : purchasesResult.getPurchasesList()){
handlePurchase(purchase);
}
});
Google Play returns the purchases made by the user account logged in to the device. If the request is successful, the Play Billing Library stores the query results in a List of Purchase objects.
Note: Only active subscriptions appear on this list. As long as the in-app product is on this list, the user should have access to it. For further information, see the Handle SUBSCRPTION_ON_HOLD section of Add subscription-specific features.
To retrieve the list, call getPurchasesList() on the PurchasesResult. You can then call a variety of methods on the Purchase object to view relevant information about the item, such as its purchase state or time. To view the types of product detail information that are available, see the list of methods in the Purchase class.
You should call queryPurchases() at least twice in your code:
Call queryPurchases() every time your app launches so that you can restore any purchases that a user has made since the app last stopped.
Call queryPurchases() in your onResume() method, because a user can make a purchase when your app is in the background (for example, redeeming a promo code in the Google Play Store app).
Calling queryPurchases() on startup and resume guarantees that your app finds out about all purchases and redemptions the user may have made while the app wasn't running. Furthermore, if a user makes a purchase while the app is running and your app misses it for any reason, your app still finds out about the purchase the next time the activity resumes and calls queryPurchases().
Query most recent purchases
The queryPurchases() method uses a cache of the Google Play Store app without initiating a network request. If you need to check the most recent purchase made by the user for each product ID, you can use queryPurchaseHistoryAsync(), passing the purchase type and a PurchaseHistoryResponseListener to handle the query result.
queryPurchaseHistoryAsync() returns a PurchaseHistory object that contains info about the most recent purchase made by the user for each product ID, even if that purchase is expired, cancelled, or consumed. Use queryPurchases() whenever possible, as it uses the local cache, instead of queryPurchaseHistoryAsync(). If using queryPurchaseHistoryAsync(), you can also combine it with a Refresh button, allowing users to update their list of purchases.
The following code demonstrates how you can override the onPurchaseHistoryResponse() method:
private void handlePurchase(Purchase purchase) {
if(purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
if (purchase.getSku().equals(skuPro)) {
EntityPRO entityPRO = RoomDB.getDatabase(context).proDAO().getLastItem();
entityPRO.isBought = true;
RoomDB.getDatabase(context).proDAO().updateSpecificSLI(entityPRO);
Toast.makeText(context, context.getString(R.string.pro_succesfully_bought), Toast.LENGTH_LONG).show();
}
if (!purchase.isAcknowledged()) {
AcknowledgePurchaseParams acknowledgePurchaseParams =
AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();
mBillingClient.acknowledgePurchase(acknowledgePurchaseParams, acknowledgePurchaseResponseListener);
}
}
}
In my application I offer the user to make a donation using Google Play IAP, in return I remove ads and unlock premium features.
When my application loads I want to check if user made a donation, how to do that via code knowing that after user makes a donation I'll call the following code to allow the user to make future donations if desired.
So, I want to allow the user to make further donations if desired, but I want to know if (s)he already made a donation to disable ads and unlock premium features.
BillingProcessor bp;
bp.consumePurchase(productId);
Note, my questions is about IAP online process not about saving a value offline and check it later.
I think this guide should help show you how to do this:
https://developer.android.com/google/play/billing/billing_library_overview
Query cached purchases
To retrieve information about purchases that a
user makes from your app, call the queryPurchases() method with the
purchase type (SkuType.INAPP or SkuType.SUBS) on the Play Billing
Library client. For example:
PurchasesResult purchasesResult = mBillingClient.queryPurchases(SkuType.INAPP);
Google Play returns the
purchases made by the user account logged in to the device. If the
request is successful, the Play Billing Library stores the query
results in a List of Purchase objects.
Note: Only active subscriptions appear on this list. As long as the
in-app product is on this list, the user should have access to it. For
further information, refer to Handle SUBSCRPTION_ON_HOLD section of
the Add subscription-specific features document. To retrieve the list,
call the getPurchasesList() method on the PurchasesResult object. You
can then call a variety of methods on the Purchase object to view
relevant information about the item, such as its purchase state or
time. To view the types of product detail information that are
available, see the list of methods in the Purchase class.
Call queryPurchases() at least twice in your code:
Every time your app launches so that you can restore any purchases
that a user has made since the app last stopped. In your onResume()
method because a user can make a purchase when your app is in the
background (for example, redeeming a promo code in Play Store app).
Calling queryPurchases() on startup and resume guarantees that your
app finds out about all purchases and redemptions the user may have
made while the app wasn't running. Furthermore, if a user makes a
purchase while the app is running and your app misses it for any
reason, your app still finds out about the purchase the next time the
activity resumes and calls queryPurchases().
Query most recent purchases
The queryPurchases() method uses a cache
of the Google Play Store app without initiating a network request. If
you need to check the most recent purchase made by the user for each
product ID, you can use the queryPurchaseHistoryAsync() method and
pass the purchase type and a PurchaseHistoryResponseListener to handle
the query result.
queryPurchaseHistoryAsync() returns the most recent purchase made by
the user for each product ID, even if that purchase is expired,
cancelled, or consumed. Use the queryPurchases() method whenever
possible, as it uses the local cache, instead of the
queryPurchaseHistoryAsync() method. You could combine
queryPurchaseHistoryAsync() with a Refresh button allowing users to
update their list of purchases.
The following code demonstrates how you can override the
onPurchaseHistoryResponse() method:
mBillingClient.queryPurchaseHistoryAsync(SkuType.INAPP,
new PurchaseHistoryResponseListener() {
#Override
public void onPurchaseHistoryResponse(#BillingResponse int responseCode,
List purchasesList) {
if (responseCode == BillingResponse.OK
&& purchasesList != null) {
for (Purchase purchase : purchasesList) {
// Process the result.
}
}
} });
You can use this:
Purchase.PurchasesResult purchasesResult = billingClient.queryPurchases(BillingClient.SkuType.SUBS); //Or SkuType.INAPP
if (purchasesResult.getPurchasesList() != null) {
for (Purchase purchase : purchasesResult.getPurchasesList()) {
if (purchase.getSku().equals("your_product_id")) handlePurchase(purchase);
}
[...]
void handlePurchase(Purchase purchase) {
if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
premium = true; //In casse purchase was acknowledge before
if (!purchase.isAcknowledged()) {
AcknowledgePurchaseParams acknowledgePurchaseParams =
AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();
AcknowledgePurchaseResponseListener acknowledgePurchaseResponseListener = new AcknowledgePurchaseResponseListener() {
#Override
public void onAcknowledgePurchaseResponse(BillingResult billingResult) {
premium = true;
}
};
billingClient.acknowledgePurchase(acknowledgePurchaseParams, acknowledgePurchaseResponseListener);
}
}
}
If you have any questions, go ahead and comment.
As nasch and AlexBSC already answered, you have to fetch for possible made purchases.
However, the most up to date method of doing this is calling BillingClient.queryPurchasesAsync() as described in here. You should at least call it in onResume() and onCreate().
for example like this,
billingClient.queryPurchasesAsync(BillingClient.SkuType.SUBS, new PurchasesResponseListener() {
#Override
public void onQueryPurchasesResponse(BillingResult billingResult, List<Purchase> purchases) {
if (billingResult.getResponseCode() == OK
&& purchases != null) {
for (Purchase purchase : purchases) {
handlePurchase(purchase);
}
}
}
});
Following these steps should get you pretty far.
I 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.
}