Skip to content

Commit

Permalink
feat: switched to billing 5.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
ch4rl3x committed Aug 13, 2022
1 parent f3987e8 commit a686c4e
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 112 deletions.
12 changes: 6 additions & 6 deletions billing/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@ apply from: '../_ktlint.gradle'

ext {
PUBLISH_GROUP_ID = 'de.charlex.billing'
PUBLISH_VERSION = '4.1.0.1'
PUBLISH_VERSION = '5.0.0.0-rc01'
PUBLISH_ARTIFACT_ID = 'billing-suspend'
}

apply from: '../_publish.gradle'

android {
compileSdkVersion 31
compileSdkVersion 33
buildToolsVersion "31.0.0"

defaultConfig {
minSdkVersion 26
targetSdkVersion 31
targetSdkVersion 33

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments clearPackageData: 'true'
Expand All @@ -47,11 +47,11 @@ dependencies {
/**
* Billing
*/
api 'com.android.billingclient:billing:4.1.0'
api 'com.android.billingclient:billing-ktx:4.1.0'
api 'com.android.billingclient:billing:5.0.0'
api 'com.android.billingclient:billing-ktx:5.0.0'

implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.core:core-ktx:1.8.0'

testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
Expand Down
55 changes: 55 additions & 0 deletions billing/src/main/java/de/charlex/billing/BillingClientExt.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package de.charlex.billing

import android.util.Log
import com.android.billingclient.api.BillingClient
import com.android.billingclient.api.BillingClientStateListener
import com.android.billingclient.api.BillingResult
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import kotlin.coroutines.resume

/**
* Starts up BillingClient setup process suspended if necessary.
*
* @return Boolean
*
* true: The billing client is ready. You can query purchases.
*
* false: The billing client is NOT ready or disconnected.
*/
suspend fun BillingClient.startConnectionIfNecessary() = suspendCancellableCoroutine<Boolean> { continuation ->
if (!isReady) {
startConnection(object : BillingClientStateListener {
override fun onBillingServiceDisconnected() {
Log.d("BillingHelper", "The billing client is disconnected.")
if (continuation.isActive) {
continuation.resume(false)
}
}

override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
Log.d("BillingHelper", "The billing client is ready. You can query purchases.")
continuation.resume(true)
} else {
Log.d("BillingHelper", "The billing client is NOT ready. ${billingResult.debugMessage}")
continuation.resume(false)
}
}
})
} else {
Log.d("BillingHelper", "The billing client is still ready")
continuation.resume(true)
}
}

/**
* Closes the connection and releases all held resources such as service connections.
*
* Call this method once you are done with this BillingClient reference.
*/
suspend fun BillingClient.endConnection() = withContext(Dispatchers.Main) {
Log.d("BillingHelper", "The billing client is still ready")
endConnection()
}
192 changes: 87 additions & 105 deletions billing/src/main/java/de/charlex/billing/BillingHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,26 @@ import android.app.Activity
import android.util.Log
import com.android.billingclient.api.AcknowledgePurchaseParams
import com.android.billingclient.api.BillingClient
import com.android.billingclient.api.BillingClientStateListener
import com.android.billingclient.api.BillingClient.ProductType
import com.android.billingclient.api.BillingFlowParams
import com.android.billingclient.api.BillingResult
import com.android.billingclient.api.ConsumeParams
import com.android.billingclient.api.ConsumeResult
import com.android.billingclient.api.InAppMessageParams
import com.android.billingclient.api.InAppMessageResult
import com.android.billingclient.api.ProductDetails
import com.android.billingclient.api.Purchase
import com.android.billingclient.api.PurchasesResult
import com.android.billingclient.api.PurchasesUpdatedListener
import com.android.billingclient.api.SkuDetails
import com.android.billingclient.api.SkuDetailsParams
import com.android.billingclient.api.QueryProductDetailsParams
import com.android.billingclient.api.QueryProductDetailsParams.Product
import com.android.billingclient.api.QueryPurchasesParams
import com.android.billingclient.api.acknowledgePurchase
import com.android.billingclient.api.consumePurchase
import com.android.billingclient.api.queryProductDetails
import com.android.billingclient.api.queryPurchasesAsync
import com.android.billingclient.api.querySkuDetails

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
Expand Down Expand Up @@ -57,51 +58,6 @@ class BillingHelper(private val activity: Activity, billingClientBuilder: Billin
)
}

/**
* Starts up BillingClient setup process suspended if necessary.
*
* @return Boolean
*
* true: The billing client is ready. You can query purchases.
*
* false: The billing client is NOT ready or disconnected.
*/
private suspend fun startConnectionIfNecessary() = suspendCancellableCoroutine<Boolean> { continuation ->
if (!billingClient.isReady) {
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingServiceDisconnected() {
Log.d("BillingHelper", "The billing client is disconnected.")
if (continuation.isActive) {
continuation.resume(false)
}
}

override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
Log.d("BillingHelper", "The billing client is ready. You can query purchases.")
continuation.resume(true)
} else {
Log.d("BillingHelper", "The billing client is NOT ready. ${billingResult.debugMessage}")
continuation.resume(false)
}
}
})
} else {
Log.d("BillingHelper", "The billing client is still ready")
continuation.resume(true)
}
}

/**
* Closes the connection and releases all held resources such as service connections.
*
* Call this method once you are done with this BillingClient reference.
*/
suspend fun endConnection() = withContext(Dispatchers.Main) {
Log.d("BillingHelper", "The billing client is still ready")
billingClient.endConnection()
}

/**
* Acknowledges in-app purchases.
*
Expand All @@ -122,7 +78,7 @@ class BillingHelper(private val activity: Activity, billingClientBuilder: Billin
*/
suspend fun acknowledgePurchase(purchaseToken: String): BillingResult? = withContext(Dispatchers.IO) {
val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder().setPurchaseToken(purchaseToken).build()
return@withContext if (startConnectionIfNecessary()) {
return@withContext if (billingClient.startConnectionIfNecessary()) {
val result = billingClient.acknowledgePurchase(acknowledgePurchaseParams)
if (result.responseCode == BillingClient.BillingResponseCode.OK) {
Log.d("BillingHelper", "Purchase acknowleged! (Token: $purchaseToken)")
Expand Down Expand Up @@ -152,7 +108,7 @@ class BillingHelper(private val activity: Activity, billingClientBuilder: Billin
*/
suspend fun consume(purchaseToken: String): ConsumeResult? = withContext(Dispatchers.IO) {
val consumeParams = ConsumeParams.newBuilder().setPurchaseToken(purchaseToken).build()
return@withContext if (startConnectionIfNecessary()) {
return@withContext if (billingClient.startConnectionIfNecessary()) {
billingClient.consumePurchase(consumeParams)
} else {
null
Expand All @@ -175,11 +131,15 @@ class BillingHelper(private val activity: Activity, billingClientBuilder: Billin
*
* @return [Purchase.PurchasesResult](https://developer.android.com/reference/com/android/billingclient/api/Purchase.PurchasesResult) The Purchase.PurchasesResult containing the list of purchases and the response code ([BillingClient.BillingResponseCode](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingResponseCode))
*/
suspend fun queryPurchases(skuType: String): PurchasesResult? = withContext(Dispatchers.IO) {
suspend fun queryPurchases(@ProductType productType: String): PurchasesResult? = withContext(Dispatchers.IO) {
Log.d("BillingHelper", "queryPurchases")
return@withContext if (startConnectionIfNecessary()) {
return@withContext if (billingClient.startConnectionIfNecessary()) {
Log.d("BillingHelper", "queryPurchases on billingClient")
billingClient.queryPurchasesAsync(skuType)
billingClient.queryPurchasesAsync(
QueryPurchasesParams.newBuilder()
.setProductType(productType)
.build()
)
} else {
null
}
Expand Down Expand Up @@ -243,37 +203,51 @@ class BillingHelper(private val activity: Activity, billingClientBuilder: Billin
* @param type String Specifies the [BillingClient.SkuType](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.SkuType) of SKUs to query.
*
*/
suspend fun purchase(sku: String, type: String, validation: suspend (Purchase) -> Boolean = { true }): PurchasesResult? {
if (startConnectionIfNecessary()) {
val skuDetails: SkuDetails? = querySkuDetails(sku, type)
skuDetails?.let {
Log.d("BillingHelper", it.toString())
suspend fun purchase(productDetails: ProductDetails, offerToken: String? = null, isOfferPersonalized: Boolean = false, validation: suspend (Purchase) -> Boolean = { true }): PurchasesResult? {
if (billingClient.startConnectionIfNecessary()) {
// val skuDetails: List<ProductDetails>? = queryProductDetails(sku, type)
// skuDetails?.let {
Log.d("BillingHelper", "purchase ${productDetails.name}")

val purchaseResult = suspendCoroutine<PurchasesResult?> { continuation ->
billingContinuation = continuation
val purchaseResult = suspendCoroutine<PurchasesResult?> { continuation ->
billingContinuation = continuation

val billingFlowParams = BillingFlowParams
.newBuilder()
.setSkuDetails(it)
.build()
billingClient.launchBillingFlow(activity, billingFlowParams)
}
val productDetailsParamsList = listOf(
BillingFlowParams.ProductDetailsParams.newBuilder()
// retrieve a value for "productDetails" by calling queryProductDetailsAsync()
.setProductDetails(productDetails)
.apply {
// to get an offer token, call ProductDetails.subscriptionOfferDetails()
// for a list of offers that are available to the user
offerToken?.let {
setOfferToken(offerToken)
}
}.build()
)

purchaseResult?.let {
purchaseResult.purchasesList.forEach { purchase ->
if (!validation(purchase)) {
Log.e("BillingHelper", "Got a purchase: $purchase; but signature is bad.")
return null
}
}
val billingFlowParams = BillingFlowParams
.newBuilder()
.setProductDetailsParamsList(productDetailsParamsList)
.setIsOfferPersonalized(isOfferPersonalized)
.build()
val result = billingClient.launchBillingFlow(activity, billingFlowParams)
result.responseCode
}

Log.d("BillingHelper", translateBillingResponseCodeToLogString(purchaseResult.billingResult.responseCode))
}
if (!purchaseResult?.billingResult?.debugMessage.isNullOrBlank()) {
Log.d("BillingHelper", "DebugMessage: ${purchaseResult?.billingResult?.debugMessage}")
purchaseResult?.let {
purchaseResult.purchasesList.forEach { purchase ->
if (!validation(purchase)) {
Log.e("BillingHelper", "Got a purchase: $purchase; but signature is bad.")
return null
}
}
return purchaseResult
} ?: return null

Log.d("BillingHelper", translateBillingResponseCodeToLogString(purchaseResult.billingResult.responseCode))
}
if (!purchaseResult?.billingResult?.debugMessage.isNullOrBlank()) {
Log.d("BillingHelper", "DebugMessage: ${purchaseResult?.billingResult?.debugMessage}")
}
return purchaseResult
} else {
return null
}
Expand All @@ -286,38 +260,46 @@ class BillingHelper(private val activity: Activity, billingClientBuilder: Billin
* @param type String Specifies the [BillingClient.SkuType](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.SkuType) of SKU to query.
*
*/
suspend fun querySkuDetails(sku: String, type: String): SkuDetails? = withContext(Dispatchers.IO) {
return@withContext querySkuDetails(
skuDetailParams = SkuDetailsParams.newBuilder().setSkusList(listOf(sku)).setType(type).build()
suspend fun queryProductDetails(productId: String, @ProductType productType: String): List<ProductDetails>? = withContext(Dispatchers.IO) {
return@withContext queryProductDetails(
productDetailsParams = QueryProductDetailsParams.newBuilder().setProductList(
listOf(
Product.newBuilder().setProductId(
productId
).setProductType(
productType
).build()
)
).build()
)
}

suspend fun querySkuDetails(skuDetailParams: SkuDetailsParams): SkuDetails? = withContext(Dispatchers.IO) {
if (skuDetailParams.skusList.size > 1) error("This function accepts only one sku per call")
if (startConnectionIfNecessary()) {
val skuDetailsResult = billingClient.querySkuDetails(skuDetailParams)
Log.d("BillingHelper", "Billing Result: ${skuDetailsResult.skuDetailsList?.size}")
return@withContext skuDetailsResult.skuDetailsList?.getOrNull(0)
suspend fun queryProductDetails(productDetailsParams: QueryProductDetailsParams): List<ProductDetails>? = withContext(Dispatchers.IO) {
// if (productDetailsParams.skusList.size > 1) error("This function accepts only one sku per call")
if (billingClient.startConnectionIfNecessary()) {
val skuDetailsResult = billingClient.queryProductDetails(productDetailsParams)
Log.d("BillingHelper", "Billing Result: ${skuDetailsResult.productDetailsList?.size}")
return@withContext skuDetailsResult.productDetailsList
} else {
return@withContext null
}
}

suspend fun querySkuDetailsList(skus: List<String>, type: String): List<SkuDetails>? = withContext(Dispatchers.IO) {
return@withContext querySkuDetailsList(
skuDetailParams = SkuDetailsParams.newBuilder().setSkusList(skus).setType(type).build()
)
}

suspend fun querySkuDetailsList(skuDetailParams: SkuDetailsParams): List<SkuDetails>? = withContext(Dispatchers.IO) {
if (startConnectionIfNecessary()) {
val skuDetailsResult = billingClient.querySkuDetails(skuDetailParams)
Log.d("BillingHelper", "Billing Result: ${skuDetailsResult.skuDetailsList?.size}")
return@withContext skuDetailsResult.skuDetailsList
} else {
return@withContext null
}
}
// suspend fun querySkuDetailsList(skus: List<String>, type: String): List<SkuDetails>? = withContext(Dispatchers.IO) {
// return@withContext querySkuDetailsList(
// skuDetailParams = SkuDetailsParams.newBuilder().setSkusList(skus).setType(type).build()
// )
// }
//
// suspend fun querySkuDetailsList(skuDetailParams: SkuDetailsParams): List<SkuDetails>? = withContext(Dispatchers.IO) {
// if (startConnectionIfNecessary()) {
// val skuDetailsResult = billingClient.querySkuDetails(skuDetailParams)
// Log.d("BillingHelper", "Billing Result: ${skuDetailsResult.skuDetailsList?.size}")
// return@withContext skuDetailsResult.skuDetailsList
// } else {
// return@withContext null
// }
// }

override fun onPurchasesUpdated(billingResult: BillingResult, purchases: List<Purchase>?) {
billingContinuation?.resume(PurchasesResult(billingResult, purchases ?: emptyList()))
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = "1.6.10"
ext.kotlin_version = "1.7.0"
repositories {
google()
mavenCentral()
Expand Down

0 comments on commit a686c4e

Please sign in to comment.