I have implemented the Billing library in order to have in-app products in my app. I can make a successful purchase and everything works fine. If I try to make a purchase again, I get the response "Product already owned" which shows that the purchase is fine. I am using as productID the test id: "android.test.purchased"
Moreover, in the Main's Activity onResume() method, I call the queryPurchases() method in order to restore the product in case the user deletes the app and re-installs it. However, this method returns an empty purchases list although the response code is OK.
This is the way I am using the method:
private fun startServiceConnectionIfNeeded(executeOnSuccess: Runnable?) {
if (mBillingClient.isReady) {
executeOnSuccess?.run()
} else {
mBillingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(#BillingClient.BillingResponse billingResponse: Int) {
if (billingResponse == BillingClient.BillingResponse.OK) {
Log.i(TAG, "onBillingSetupFinished() response: $billingResponse")
executeOnSuccess?.run()
} else {
Log.w(TAG, "onBillingSetupFinished() error code: $billingResponse")
}
}
override fun onBillingServiceDisconnected() {
Log.w(TAG, "onBillingServiceDisconnected()")
}
})
}
}
// I call this method in Main's onResume()
fun updateBillingSharedPreferencesKey() {
val executeOnConnectedService = Runnable {
val purchasesResult = mBillingClient.queryPurchases(BillingClient.SkuType.INAPP)
val purchasesList = purchasesResult?.purchasesList
purchasesList?.let {
// this list is always empty, although I have a valid product
for (purchase in it) {
// I never enter this loop to handle the product functionality
Log.d(TAG, "purchase.sku = ${purchase?.sku}")
}
}
}
startServiceConnectionIfNeeded(executeOnConnectedService)
}
Am I doing something wrong? The service connection and the response code is ok.
How should I call the queryPurchases() method to get my valid in-app product?
Related
I have an issue that is Payment is successful but addSubscriptionListener and addPurchaseListener not call
I am using this library:-
implementation 'com.github.akshaaatt:Google-IAP:1.2.5'
From this link:-
https://github.com/akshaaatt/Google-IAP
Here the listener is working after clicking twice on item to purchase.
Here is the listener:-
iapConnector.addPurchaseListener(object : PurchaseServiceListener {
override fun onPricesUpdated(iapKeyPrices: Map<String, DataWrappers.SkuDetails>) {
// list of available products will be received here, so you can update UI with prices if needed
}
override fun onProductPurchased(purchaseInfo: DataWrappers.PurchaseInfo) {
// will be triggered whenever purchase succeeded
}
override fun onProductRestored(purchaseInfo: DataWrappers.PurchaseInfo) {
// will be triggered fetching owned products using IapConnector
}
})
iapConnector.addSubscriptionListener(object : SubscriptionServiceListener {
override fun onSubscriptionRestored(purchaseInfo: DataWrappers.PurchaseInfo) {
// will be triggered upon fetching owned subscription upon initialization
}
override fun onSubscriptionPurchased(purchaseInfo: DataWrappers.PurchaseInfo) {
// will be triggered whenever subscription succeeded
}
override fun onPricesUpdated(iapKeyPrices: Map<String, DataWrappers.SkuDetails>) {
// list of available products will be received here, so you can update UI with prices if needed
}
})
I have put in the button click listener but still it is not called at once.
I have a situation where I want the livedata to be observed only once in the app. The problem is that I am working on the authentication for an app using some Node.js backend.
As I am sending the values to receive the response from the backend it's working fine till now. I observe that response and based on that I make changes to my fragment ( that is if the response received is true then move to next fragment, otherwise if it is false show a toast message ).
Now the problem is that :
Case 1: I opened the app, entered the right credentials and pressed the button, received true response from the server and goes to the next fragment.
Case 2: I opened the app, but entered the wrong credentials, I received a false from server and based on that the Toast is shown.
Case 3 (The issue): I opened the app, entered the wrong credentials and then without closing the fragment screen entered the right credentials by editing them, the app crashes and at the same time I receive multiple responses from the server via LiveData.
My observation: Looking more into that I found that the LiveData is attached to the fragment/activity and therefore it shows the last state. So as in case 3 the the last state was receiving the false value from backend it was used again and we were shown the error instead of going to the next screen.
Can anyone guide me how to solve this. Thanks
Some code that might be needed:
binding.btnContinue.setOnClickListener {
val number = binding.etMobileNumber.text.toString().toLong()
Timber.d("Number: $number")
authRiderViewModel.authDriver(number)
checkNumber()
}
Function which checks the number :
private fun checkNumber() {
authRiderViewModel.response.observe(viewLifecycleOwner, Observer {
Timber.d("Response: $it")
if (it!!.success == true) {
val action = LoginFragmentDirections.actionLoginFragmentToOtpFragment()
findNavController().navigate(action)
Timber.d("${it.message}")
} else {
Toast.makeText(requireContext(), "Number not registered", Toast.LENGTH_SHORT).show()
binding.etMobileNumber.setText("")
}
})
}
ViewModel code:
private val _response = MutableLiveData<AuthResponse>()
val response: LiveData<AuthResponse>
get() = _response
fun authDriver(number: Long) = viewModelScope.launch {
Timber.d("Number: $number")
myRepo.authDriver(number).let {
_response.postValue(it)
}
}
P.S I have tried using something called SingleLiveEvent but it doesn't seem to work.
I would create a separate class that tracks the UI state you need and update it when the state is consumed. Something like the following. I don't really know what the parameter is for authDriver, so this is a more generic example.
sealed interface AuthState {
object NotYetRequested: AuthState
object AwaitingResponse: AuthState
class ResponseReceived(val response: AuthResponse): AuthState {
var isHandled = false
private set
fun markHandled() {
isHandled = true
}
}
}
// In ViewModel:
private val _authState = MutableLiveData<AuthState>().also {
it.value = AuthState.NotYetRequested
}
val authState: LiveData<AuthState> get() = _authState
fun requestAuthentication() = viewModelScope.launch {
_authState.value = AuthState.AwaitingResponse
val response = myRepo.authenticate()
_authState.value = AuthState.ResponseReceived(response)
}
// In Fragment:
viewModel.authState.observe(viewLifecycleOwner) { authState ->
when (authState) {
AuthState.NotYetRequested -> ShowUiRequestingAuthentication()
AuthStateAwaitingResponse -> ShowIndeterminateProgressUi()
is AuthStateResponseReceived -> when {
authState.isHandled -> {} // do nothing? depends on your setup, might need to navigate to next screen if handled response is successful
authState.response.isSuccessful -> {
goToNextScreen()
authState.markHandled()
}
else -> {
showErrorToast()
ShowUiRequestingAuthentication()
authState.markHandled()
}
}
}
}
I am new to android and trying to implement in-app billing the first time.
I am using the google play in-app library. https://developer.android.com/google/play/billing/billing_library_overview
I want to implement subscription in-app purchase in kotlin.
I'm not able to resolve the issue: Suspend function 'acknowledgePurchase' should be called only from a coroutine or another suspend function. How can I call this function?
Here is my handlePurchase method:
fun handlePurchase(purchase: Purchase) {
if (purchase.purchaseState === Purchase.PurchaseState.PURCHASED) {
if (!purchase.isAcknowledged) {
val params =
AcknowledgePurchaseParams.newBuilder().setPurchaseToken(purchase.purchaseToken)
billingClient.acknowledgePurchase(params.build()) }
}
}
Assuming you are using kotlin coroutines in your app.
BillingClient#acknowledgePurchase(params.build()) is a suspended function that should only be called from another suspend function or coroutine scope.
Let's say you are using this fun handlePurchase(purchase: Purchase) in a fragment, you can use viewLifecycleOwner.lifecycleScope.launch{} to launch the same.
fun handlePurchase(purchase: Purchase) {
if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
if (!purchase.isAcknowledged) {
val params = AcknowledgePurchaseParams.newBuilder().setPurchaseToken(purchase.purchaseToken)
viewLifecycleOwner.lifecycleScope.launch {
val result = billingClient.acknowledgePurchase(params.build())
when (result.responseCode) {
BillingClient.BillingResponseCode.OK -> {
//success flow}
}
else -> {
//error flow
}
}
}
}
}
}
I am trying to add a subscription to my app. I have created a subscription called "pro_subscription" in the Google Play Console and made sure it's enabled. I then created a new Alpha build and released it to myself, with an account I have configured for testing.
We added com.android.vending.BILLING to the permissions and already had the internet permission.
No matter what we've tried, when we try to fetch the subscription in order to purchase it, no results are found.
We have tried to fetch using Android APIs directly, in which mutableList?.isEmpty() is true:
fun querySkuDetails() {
val purchasesUpdatedListener =
PurchasesUpdatedListener { billingResult, purchases ->
// To be implemented in a later section.
}
var billingClient = BillingClient.newBuilder(activity as MainActivity)
.setListener(purchasesUpdatedListener)
.enablePendingPurchases()
.build()
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
// The BillingClient is ready. You can query purchases here.
val skuList = ArrayList<String>()
skuList.add("subscription")
skuList.add("pro_subscription")
val params = SkuDetailsParams.newBuilder()
params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP)
billingClient.querySkuDetailsAsync(params.build(), SkuDetailsResponseListener() { billingResult: BillingResult, mutableList: MutableList<SkuDetails>? ->
if (mutableList?.isEmpty()!!) {
Log.d("OFFERS: ", "mutableList is empty")
return#SkuDetailsResponseListener
}
})
}
}
override fun onBillingServiceDisconnected() {
// Try to restart the connection on the next request to
// Google Play by calling the startConnection() method.
}
})
And we also tried using the third-party revenuecat by configuring an offering and then fetching it, in which offerings.all.isEmpty() is true:
Purchases.sharedInstance.getOfferingsWith(
onError = { error ->
/* Optional error handling */
throw IllegalArgumentException()
},
onSuccess = { offerings ->
if (offerings.current == null) {
Log.d("OFFERS: ", "offerings.current is null")
}
if (offerings.all.isEmpty()) {
Log.d("OFFERS: ", "offerings.all.size is 0")
}
// Display current offering with offerings.current
Purchases.sharedInstance.purchasePackageWith(
activity as MainActivity,
offerings.current?.getPackage("pro")!!,
onError = { error, userCancelled -> /* No purchase */
if (userCancelled) {
val requestCancelledToast = Toast.makeText(
context,
R.string.purchase_request_cancelled_toast,
Toast.LENGTH_LONG
)
requestCancelledToast.show()
} else {
throw IllegalArgumentException()
}
},
onSuccess = { product, purchaserInfo ->
if (purchaserInfo.entitlements["pro"]?.isActive == true) {
// Unlock that great "pro" content
try {
val adView =
container!!.findViewById<AdView>(R.id.adView)
(adView.parent as ViewGroup).removeView(
adView
)
} catch (e: Exception) {
val errorRemoveToast = Toast.makeText(
context,
R.string.error_remove_ad,
Toast.LENGTH_LONG
)
errorRemoveToast.show()
}
}
})
})
In both cases, no error is printed, caught, or otherwise identified.
Anyone know why we are not able to fetch the purchases based on the above steps? Thanks!
Did you add Library of billing on the app's gradle?
It is;
dependencies {
val billing_version = "4.0.0"
implementation("com.android.billingclient:billing-ktx:$billing_version")
}
I'm need to get GCLID value inside my android app. What i found is Play Install Referrer Library, that returns me a utm tags, but i'm not sure that GCLID value will be inside val referrer = response.installReferrer
Code sample:
fun getGCLID(){
referrerClient = InstallReferrerClient.newBuilder(this).build()
referrerClient.startConnection(object : InstallReferrerStateListener {
override fun onInstallReferrerSetupFinished(responseCode: Int) {
when (responseCode) {
InstallReferrerClient.InstallReferrerResponse.OK -> {
// Connection established.
val response: ReferrerDetails = referrerClient.installReferrer
val referrer = response.installReferrer
val clickTimestamp = response.referrerClickTimestampSeconds
val installTimestamp = response.installBeginTimestampSeconds
Log.d("TagGCLID", "onInstallReferrerSetupFinished: 1")
if ("gclid" in referrer) {
Log.d("TagGCLID", "GCLID is detected in referrer // is $referrer")
//report to Firebase Analytics
} else {
Log.d("TagGCLID", "no GCLID is detected in referrer\n" +
"$referrer")
//do something else
}
}
InstallReferrerClient.InstallReferrerResponse.FEATURE_NOT_SUPPORTED -> {
Log.d("TagGCLID", "InstallReferrerResponse: -1")
// API not available on the current Play Store app.
}
InstallReferrerClient.InstallReferrerResponse.SERVICE_UNAVAILABLE -> {
Log.d("TagGCLID", "InstallReferrerResponse: -2")
// Connection couldn't be established.
}
}
}
override fun onInstallReferrerServiceDisconnected() {
Log.d("TagGCLID", "onInstallReferrerServiceDisconnected: -3")
// Try to restart the connection on the next request to
// Google Play by calling the startConnection() method.
}
})
}
What i'm suppose to do to get GCLID value?
yes, I also have a similar problem, I can get this gclid if the user has switched from the browser to the play market, but if the advertisement was clicked in the play market itself, then the gclid is not transmitted. Your method is written correctly, but it only works if the user goes through a browser.