I'm learning JUNIT tests and wanted to create a test for purchases.
I do not understand how can I not "fake" generate result of google API function querySkuDetailsAsync which returns responseCode and list of skuDetails which I then pass to calculatePrice() for instance. How do I get these results in test function?.
fun getPriceAndProductId(
obj: Object,
listener: (price: Double, productId: String) -> Unit
) {
val skus = listOf(
sku1,
sku2,
sku3)
billingManager.querySkuDetailsAsync(
BillingClient.SkuType.INAPP,
skus,
SkuDetailsResponseListener { responseCode, skuDetailsList ->
if (responseCode == BillingClient.BillingResponse.OK) {
val price = calculatePrice(...)
val productId = calculateProductId(...)
} else {
LogUtil.debug("...", "Unable to get SKUS: $responseCode")
}
})
}
JUnit:
#Test
fun testProductPrice() {
val skuDetailsList = /* somehow call google API and get the skus */
assertEquals(objectClass.calculatePrice(skuDetailsList), "0.0")
}
Related
I have integrated the google play billing library inside my application to make a subscription purchase. It works well. The user receives purchase confirmation mail from google.
confirmation mail from google
The active subscription is also visible in the play store subscriptions screen.
Active subscription showing in play store
In the play console, the order detail said the user was successfully charged for a subscription. However, It gets canceled instantly.
play console order history
I also check purchase acknowledgment, it returns true on every purchase. Initially, I thought there would be a mistake in my code. So, I tried various billing libraries from Github. The problem persists. In the end, I replaced the entire google billing library with Revenue cat. Followed every step described on Revenue cat documents. Still, getting the same issue.
Is there anything that I am missing to implement or done incorrectly? please help me out. Thank you
code for fetching available products:
private fun fetchOffering(){
Purchases.sharedInstance.getOfferingsWith({ error ->
// An error occurred
handleBillingError(requireActivity(), error)
}) { offerings ->
offerings.current?.availablePackages?.takeUnless { it.isNullOrEmpty() }?.let {
// All packages from current offering
if (it.isNotEmpty()){
it.forEach { p: Package ->
offeringPackages.add(p)
}
isProductsAvailable = true
}
Log.d("RevenueCat", "fetchOffering: success: ${it.size}")
}
}
}
code for making purchase:
private fun makePurchase(pack:Package){
Purchases.sharedInstance.purchasePackageWith(
requireActivity(),
packageToPurchase = pack /* package from the fetched Offering*/,
onError = {error, userCancelled ->
Log.d("RevenueCat", "makePurchase: userCancelled: $userCancelled")
handleBillingError(requireActivity(), error)
},
onSuccess = { product, purchaserInfo ->
if (purchaserInfo.entitlements[REVENUE_ENTITLEMENT_ID_PRO]?.isActive == true) {
Log.d("RevenueCat", "makePurchase: success: ${product.originalJson} ")
afterPurchaseSuccessSetup()
}
})
}
I don't know where you are lagging, you should follow these steps to make subscribe purchase.
private lateinit var billingClient: BillingClient
private val skuListsubscribe = listOf("subscription_1", "subscription_2")
initialize billing client.
private fun setupBillingClient() {
billingClient = BillingClient.newBuilder(BaseActivity.mContext!!)
.enablePendingPurchases()
.setListener(this)
.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.
loadAllSKUsSubscription()
}
}
override fun onBillingServiceDisconnected() {
// Try to restart the connection on the next request to
// Google Play by calling the startConnection() method.
Log.e("TAG", "onBillingServiceDisconnected: ")
}
})
}
private fun loadAllSKUsSubscription() = if (billingClient.isReady) {
val params = SkuDetailsParams
.newBuilder()
.setSkusList(skuListsubscribe)
.setType(BillingClient.SkuType.SUBS)
.build()
billingClient.querySkuDetailsAsync(params) { billingResult, skuDetailsList ->
// Process the result.
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK && skuDetailsList!!.isNotEmpty()) {
for (skuDetails in skuDetailsList) {
if (skuDetails.sku == subscribtionId) {
tv_full_category_price.text = skuDetails.price
realAmountSubscription = skuDetails.price
rl_bestvalue_plan.setOnClickListener {
isInAppPurchase = false
isSubscribtion = true
val billingFlowParams = BillingFlowParams
.newBuilder()
.setSkuDetails(skuDetails)
.build()
billingClient.launchBillingFlow(requireActivity(), billingFlowParams)
}
}
}
}
}
} else {
println("Billing Client not ready")
}
override purchase update method
override fun onPurchasesUpdated(
billingResult: BillingResult,
purchases: MutableList<Purchase>?,
) {
if (billingResult?.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) {
for (purchase in purchases) {
acknowledgePurchase(purchase)
}
} else if (billingResult?.responseCode == BillingClient.BillingResponseCode.USER_CANCELED) {
//logger("User Cancelled")
//logger(billingResult?.debugMessage.toString())
} else {
//logger(billingResult?.debugMessage.toString())
}
}
private fun acknowledgePurchase(purchase: Purchase) {
if (isSubscribtion) {
handleNonConsumableProduct(purchase = purchase)
}
}
fun handleConsumableProduct(purchase: Purchase) {
val consumeParams =
ConsumeParams.newBuilder()
.setPurchaseToken(purchase.purchaseToken)
.build()
billingClient.consumeAsync(consumeParams) { billingResult, purchaseToken ->
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
if (isVisible && isAdded)
Snackbar.MakeInternetSnackbar(BaseActivity.mContext!!,
subscription_fragment_indicator_layout,
"Purchased Successfully")
InAppPaymentApi(realAmount.toString(), audio_id!!, purchase.purchaseToken)
}
}
}
I am migrating from v1.0 to v3.0 of play billing library.
The code below demonstrates how to launch a purchase flow in v1.0
val billingParams = BillingFlowParams.newBuilder().setSku(skuId).setType(skuType).build()
val billingResult = billingClient.launchBillingFlow(activity, billingParams)
In version 3.0
it is meant to be done like this:
val billingParams = BillingFlowParams.newBuilder().setSkuDetails(skuDetails).build()
val billingResult = billingClient.launchBillingFlow(activity, billingParams)
how do I create skuDetail with skuId and skuType?
Here is the method I use , you pass list of package names and it returns list of SkuDetails
suspend fun queryInAppPurchaseSkuDetails(packagesList: List<String>): List<SkuDetails>? {
val params = SkuDetailsParams.newBuilder()
params.setSkusList(packagesList).setType(BillingClient.SkuType.INAPP)
val skuDetailsResult = withContext(Dispatchers.IO) {
billingClient?.querySkuDetails(params.build())
}
// Process the result.
if (skuDetailsResult?.billingResult?.responseCode == BillingClient.BillingResponseCode.OK && packagesList.isNotNullOrEmpty()) {
val skuDetailsList = skuDetailsResult.skuDetailsList
return skuDetailsList
}
return null
}
I'am trying to use rxBilling for in app purchases on my android app. library found here : https://github.com/betterme-dev/RxBilling
i manage to implemente it like this :
private fun launchBilling(subscription: Boolean) {
val sku = "com.app.subscription"
disposable.add(rxBilling.launchFlow(
this, BillingFlowParams.newBuilder()
.setSku(sku)
.setType(BillingClient.SkuType.SUBS)
.build()
)
.subscribe({
}, {
it.message?.let { msg -> unknownError(0, msg) }
})
)
}`
This is working perfectly, i pushed my app in internal test and i tried to subscribe, it's worked and i verified in my google play console that my subscription was taken into account.
The problem is that i want to get information of the purchase like date and transaction id. So i followed the guideline and implemented a disposable in the on start method :
override fun onStart() {
super.onStart()
println("onstrat")
disposable.add(
rxBilling.observeUpdates()
.subscribe({
println("here")
it.purchases.forEach { item ->
println("rxbillingstart")
println(item.toString())
println(item)
println(item.sku)
purchase(item)
}
}, {
togglePurchaseProgress(false)
it.message?.let { msg -> unknownError(0, msg) }
})
)
}
And for the implementation of the rxbilling and disposable in the on create method :
private lateinit var rxBilling: RxBilling
private lateinit var rxBillingFlow: RxBillingFlow
private val disposable = CompositeDisposable()
rxBilling = RxBillingImpl(BillingClientFactory(applicationContext))
rxBillingFlow = RxBillingFlow(applicationContext, BillingServiceFactory(this))
lifecycle.addObserver(BillingConnectionManager(rxBilling))
lifecycle.addObserver(BillingConnectionManager(rxBillingFlow))
The purchase methode is juste here to print item.orderId date ... but it's never called. Also my first print "onstrat" in the onStart method is called but all other print in the observable.subscribe are never called. Do I miss something? i don't get why i can't get subsciption informations
I found out that RxBilling is no longer maintained, so i used standard android billing client found here : https://developer.android.com/google/play/billing/integrate .But don't forget to add to your manifest :
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="com.android.vending.BILLING"/>
Then implemente those interface in your activity : PurchasesUpdatedListener, SkuDetailsResponseListener and set up the billing client variable : private lateinit var billingClient : BillingClient . Also it was unclear to me at first but sku is the product id you set up on googleplay console.
private fun buy(){
billingClient = BillingClient.newBuilder(this)
.setListener(this)
.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("premium_upgrade")
skuList.add("com.yourappname.subscription")
val params = SkuDetailsParams.newBuilder()
params.setSkusList(skuList).setType(BillingClient.SkuType.SUBS)
billingClient.querySkuDetailsAsync(params.build(), this#DetailsActivity)
}
}
override fun onBillingServiceDisconnected() {
// Try to restart the connection on the next request to
// Google Play by calling the startConnection() method.
}
})
}
Set up the skudetails you are searching for like this, then get the response here :
override fun onSkuDetailsResponse(responseCode: BillingResult, skuDetailsList: MutableList<SkuDetails>?) {
if (responseCode.responseCode != BillingClient.BillingResponseCode.OK) {
Log.w("LOG_TAG", "SkuDetails query failed with response: $responseCode")
} else {
Log.d("LOG_TAG", "SkuDetails query responded with success. List: $skuDetailsList")
if (skuDetailsList != null) {
for (skuDetails in skuDetailsList) {
var sku = skuDetails.getSku ()
var price = skuDetails.getPrice ()
var params = BillingFlowParams.newBuilder()
.setSkuDetails(skuDetails)
.build();
if (sku == "com.yourappname.subscription") {
billingClient.launchBillingFlow(this, params);
}
}
}
}
}
This mean that if i found the right sku i lauchBillingflow -> i start the process of paying. Then you get the result on the payement here :
override fun onPurchasesUpdated(billingResult: BillingResult, purchases: MutableList<Purchase>?) {
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) {
for (purchase in purchases) {
purchase(1, purchase)
}
} else if (billingResult.responseCode == BillingClient.BillingResponseCode.USER_CANCELED) {
// Handle an error caused by a user cancelling the purchase flow.
} else {
// Handle any other error codes.
}
}
You can here get the result on your purchase such as purchaseTime, orderId, purchaseToken, and then my function purchase send the purchaseToken to an internal server to verify the transaction.
Hope it's will help some of you.
This is not a question actually. I spent some time on this, so I am putting here the steps involved to make an in-app purchase.
added dependencies
//billing-client
implementation 'com.android.billingclient:billing:2.2.1'
implementation 'com.android.billingclient:billing-ktx:2.2.1'
added permission in manifest
Created application in Play Console.
Play Console -> settings -> Pricing Template :: added pricing template.
Select Application Form Console -> Store Presence -> In-App Products -> Managed
Products -> Create a Managed Product.
For Testing, first of all, you need to release a build as Alpha (with some testing mail id registered in closed track.)
After Successful publication you can use the same build (maybe needed the same version code and version number) (Both debug and Release works) for test the purchase.
Ensure the testing mail IDs are google ids (#gmail.com)
Here are the code snippets that I have used for billing.
implement PurchasesUpdatedListener, on fragment or activity
Setup BIlling Client
private fun setupBillingClient() {
billingClient = BillingClient.newBuilder(mainActivity)
.enablePendingPurchases()
.setListener(this)
.build()
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
// The BillingClient is setup
log("Setup Billing Done")
loadAllSKUs()
}
}
override fun onBillingServiceDisconnected() {
log("Failed")
}
})
}
Load All Skus (Ids of your purchasing templates (use arraylist))
private fun loadAllSKUs() = if (billingClient.isReady) {
val params = SkuDetailsParams
.newBuilder()
.setSkusList(skuList)
.setType(BillingClient.SkuType.INAPP)
.build()
billingClient.querySkuDetailsAsync(params) { billingResult, skuDetailsList ->
// Process the result.
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK && skuDetailsList.isNotEmpty()) {
for (skuDetails in skuDetailsList) {
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK && skuDetailsList.isNotEmpty()) {
this.skuDetailsList = skuDetailsList
}
}
}
}
} else {
log("Billing Client not ready")
}
Make Google Payment
private fun invokeGooglePayPayment(skuDetails: SkuDetails) {
val billingFlowParams = BillingFlowParams
.newBuilder()
.setSkuDetails(skuDetails)
.build()
billingClient.launchBillingFlow(mainActivity, billingFlowParams)
}
If you need to purchase again on the same in-app purchase item you can use this code,
override fun onPurchasesUpdated(
billingResult: BillingResult?,
purchases: MutableList<Purchase>?
) {
//todo remove this function only for token log
if (!purchases.isNullOrEmpty()) {
for (purchase in purchases) {
Log.e("Purchase Token" + purchase.purchaseToken, "OK")
}
}
if (billingResult?.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) {
for (purchase in purchases) {
acknowledgePurchase(purchase) // do after purchase
}
} else if (billingResult?.responseCode == BillingClient.BillingResponseCode.USER_CANCELED) {
//todo
} else if (billingResult?.responseCode == BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED) {
val consumeParams = ConsumeParams.newBuilder()
.setPurchaseToken("inapp:${BuildConfig.APPLICATION_ID}:${selectedSkuDetails?.sku}")
.build()
runBlocking {
CoroutineScope(Dispatchers.IO).launch {
billingClient.consumePurchase(consumeParams)
}
MainScope().launch {
invokeGooglePayPayment(selectedSkuDetails!!)
}
}
}
}
billing lib.: 'com.android.billingclient:billing:2.0.3'
starting a flow after a successful startConnection ,
val skuList = ArrayList<String>()
skuList.add("buy4")
val params = SkuDetailsParams.newBuilder()
params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP)
//billingClient is declared and initialized earlier
billingClient.querySkuDetailsAsync(params.build())
{ billingResult, skuDetailsList ->
val flowParams = BillingFlowParams.newBuilder()
.setSkuDetails(skuDetailsList.first())
.build()
val responseCode = billingClient.launchBillingFlow(this, flowParams)
println(responseCode.responseCode) //prints 0 ... OK
}
the MainActivity implements PurchasesUpdatedListener
override fun onPurchasesUpdated(billingResult: BillingResult?, purchases: MutableList<Purchase>?) {
if (billingResult?.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) {
for (purchase in purchases) {
acknowledgePurchase(purchase)
}
}
}
private fun acknowledgePurchase(purchase: Purchase) {
if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
// Grant entitlement to the user.
// Acknowledge the purchase
val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.purchaseToken)
.setDeveloperPayload("PayloadString")
.build()
billingClient.acknowledgePurchase(
acknowledgePurchaseParams,
object : AcknowledgePurchaseResponseListener {
override fun onAcknowledgePurchaseResponse(billingResult: BillingResult?) {
println("payload =${purchase.developerPayload}") // prints "payload="
}
})
}
}
the developer payload is empty, despite being set at the AcknowledgePurchaseParams , I also saved the purchase after acknowledging it, and tried calling purchase.developerPayload after a while, and still it's blank , what are the best practices for using developer payload to verify in app purchases ?
N.B I'm using an internal testing track on play console
In onAcknowledgePurchaseResponse you will need to refresh your purchase object from the cache. The cache is updated by the time onAcknowledgePurchaseResponse is called so you do this by calling https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#querypurchases. We will consider adding the update purchase to the listener for a future library release to make this more convenient.