Update Google Play Billing to 7.1.1

pull/3971/head
Alex Baker 4 weeks ago
parent 20e995b19b
commit 4c492120b3

@ -4,27 +4,24 @@ import android.app.Activity
import android.content.Context import android.content.Context
import com.android.billingclient.api.AcknowledgePurchaseParams import com.android.billingclient.api.AcknowledgePurchaseParams
import com.android.billingclient.api.BillingClient.BillingResponseCode import com.android.billingclient.api.BillingClient.BillingResponseCode
import com.android.billingclient.api.BillingClient.SkuType import com.android.billingclient.api.BillingClient.ProductType
import com.android.billingclient.api.BillingClient.newBuilder import com.android.billingclient.api.BillingClient.newBuilder
import com.android.billingclient.api.BillingClientStateListener import com.android.billingclient.api.BillingClientStateListener
import com.android.billingclient.api.BillingFlowParams import com.android.billingclient.api.BillingFlowParams
import com.android.billingclient.api.BillingFlowParams.ProrationMode import com.android.billingclient.api.BillingFlowParams.ProductDetailsParams
import com.android.billingclient.api.BillingFlowParams.SubscriptionUpdateParams import com.android.billingclient.api.BillingFlowParams.SubscriptionUpdateParams
import com.android.billingclient.api.BillingResult import com.android.billingclient.api.BillingResult
import com.android.billingclient.api.ConsumeParams import com.android.billingclient.api.ConsumeParams
import com.android.billingclient.api.Purchase.PurchaseState import com.android.billingclient.api.Purchase.PurchaseState
import com.android.billingclient.api.PurchasesResult
import com.android.billingclient.api.PurchasesUpdatedListener import com.android.billingclient.api.PurchasesUpdatedListener
import com.android.billingclient.api.SkuDetailsParams import com.android.billingclient.api.QueryProductDetailsParams
import com.android.billingclient.api.QueryPurchasesParams
import com.android.billingclient.api.consumePurchase import com.android.billingclient.api.consumePurchase
import com.android.billingclient.api.queryPurchasesAsync
import com.android.billingclient.api.querySkuDetails
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import org.tasks.BuildConfig import org.tasks.BuildConfig
import org.tasks.analytics.Firebase import org.tasks.analytics.Firebase
import org.tasks.jobs.WorkManager import org.tasks.jobs.WorkManager
@ -49,32 +46,61 @@ class BillingClientImpl(
override suspend fun getSkus(skus: List<String>): List<Sku> = override suspend fun getSkus(skus: List<String>): List<Sku> =
executeServiceRequest { executeServiceRequest {
val skuDetailsResult = withContext(Dispatchers.IO) { val productList = skus.map {
billingClient.querySkuDetails( QueryProductDetailsParams.Product.newBuilder()
SkuDetailsParams .setProductId(it)
.newBuilder() .setProductType(ProductType.SUBS)
.setType(SkuType.SUBS)
.setSkusList(skus)
.build() .build()
)
} }
skuDetailsResult.billingResult.let { val params = QueryProductDetailsParams.newBuilder()
.setProductList(productList)
.build()
val productDetailsResult = withContext(Dispatchers.IO) {
suspendCoroutine { cont ->
billingClient.queryProductDetailsAsync(params) { billingResult, productDetailsList ->
cont.resume(billingResult to productDetailsList)
}
}
}
productDetailsResult.first.let {
if (!it.success) { if (!it.success) {
throw IllegalStateException(it.responseCodeString) throw IllegalStateException(it.responseCodeString)
} }
} }
val json = Json { ignoreUnknownKeys = true }
skuDetailsResult productDetailsResult.second?.map { productDetails ->
.skuDetailsList Sku(
?.map { json.decodeFromString<Sku>(it.originalJson) } productId = productDetails.productId,
?: emptyList() price = productDetails.subscriptionOfferDetails?.firstOrNull()?.pricingPhases?.pricingPhaseList?.firstOrNull()?.formattedPrice
?: productDetails.oneTimePurchaseOfferDetails?.formattedPrice
?: ""
)
} ?: emptyList()
} }
override suspend fun queryPurchases(throwError: Boolean) = try { override suspend fun queryPurchases(throwError: Boolean) = try {
executeServiceRequest { executeServiceRequest {
withContext(Dispatchers.IO + NonCancellable) { withContext(Dispatchers.IO + NonCancellable) {
val subs = billingClient.queryPurchasesAsync(SkuType.SUBS) val subsParams = QueryPurchasesParams.newBuilder()
val iaps = billingClient.queryPurchasesAsync(SkuType.INAPP) .setProductType(ProductType.SUBS)
.build()
val iapsParams = QueryPurchasesParams.newBuilder()
.setProductType(ProductType.INAPP)
.build()
val subs = suspendCoroutine { cont ->
billingClient.queryPurchasesAsync(subsParams) { billingResult, purchases ->
cont.resume(PurchasesResult(billingResult, purchases))
}
}
val iaps = suspendCoroutine { cont ->
billingClient.queryPurchasesAsync(iapsParams) { billingResult, purchases ->
cont.resume(PurchasesResult(billingResult, purchases))
}
}
if (subs.success || iaps.success) { if (subs.success || iaps.success) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
inventory.clear() inventory.clear()
@ -105,7 +131,7 @@ class BillingClientImpl(
purchases?.forEach { purchases?.forEach {
firebase.reportIabResult( firebase.reportIabResult(
result.responseCodeString, result.responseCodeString,
it.skus.joinToString(","), it.products.joinToString(","),
it.purchaseState.purchaseStateString it.purchaseState.purchaseStateString
) )
} }
@ -122,31 +148,57 @@ class BillingClientImpl(
oldPurchase: Purchase? oldPurchase: Purchase?
) { ) {
executeServiceRequest { executeServiceRequest {
val skuDetailsResult = withContext(Dispatchers.IO) { val productList = listOf(
billingClient.querySkuDetails( QueryProductDetailsParams.Product.newBuilder()
SkuDetailsParams.newBuilder().setSkusList(listOf(sku)).setType(skuType) .setProductId(sku)
.setProductType(skuType)
.build() .build()
) )
val queryParams = QueryProductDetailsParams.newBuilder()
.setProductList(productList)
.build()
val productDetailsResult = withContext(Dispatchers.IO) {
suspendCoroutine { cont ->
billingClient.queryProductDetailsAsync(queryParams) { billingResult, productDetailsList ->
cont.resume(billingResult to productDetailsList)
}
}
} }
skuDetailsResult.billingResult.let {
productDetailsResult.first.let {
if (!it.success) { if (!it.success) {
throw IllegalStateException(it.responseCodeString) throw IllegalStateException(it.responseCodeString)
} }
} }
val skuDetails =
skuDetailsResult val productDetails = productDetailsResult.second?.firstOrNull()
.skuDetailsList ?: throw IllegalStateException("Product $sku not found")
?.firstOrNull()
?: throw IllegalStateException("Sku $sku not found") val productDetailsParamsBuilder = ProductDetailsParams.newBuilder()
val params = BillingFlowParams.newBuilder().setSkuDetails(skuDetails) .setProductDetails(productDetails)
// For subscriptions (including legacy subscriptions), we need to provide an offer token
if (skuType == ProductType.SUBS) {
val offerToken = productDetails.subscriptionOfferDetails?.firstOrNull()?.offerToken
?: throw IllegalStateException("No offer token found for subscription $sku")
productDetailsParamsBuilder.setOfferToken(offerToken)
}
val productDetailsParams = productDetailsParamsBuilder.build()
val params = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(listOf(productDetailsParams))
oldPurchase?.let { oldPurchase?.let {
params.setSubscriptionUpdateParams( params.setSubscriptionUpdateParams(
SubscriptionUpdateParams.newBuilder() SubscriptionUpdateParams.newBuilder()
.setOldSkuPurchaseToken(it.purchaseToken) .setOldPurchaseToken(it.purchaseToken)
.setReplaceSkusProrationMode(ProrationMode.IMMEDIATE_WITH_TIME_PRORATION) .setSubscriptionReplacementMode(BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.WITH_TIME_PRORATION)
.build() .build()
) )
} }
if (activity is OnPurchasesUpdated) { if (activity is OnPurchasesUpdated) {
onPurchasesUpdated = activity onPurchasesUpdated = activity
} }
@ -214,17 +266,28 @@ class BillingClientImpl(
ConsumeParams.newBuilder().setPurchaseToken(purchase.purchaseToken).build(), ConsumeParams.newBuilder().setPurchaseToken(purchase.purchaseToken).build(),
) )
Timber.d("consume purchase: ${result.billingResult.responseCodeString}") Timber.d("consume purchase: ${result.billingResult.responseCodeString}")
queryPurchases() queryPurchases(throwError = false)
} }
} }
private data class PurchasesResult(
val billingResult: BillingResult,
val purchasesList: List<com.android.billingclient.api.Purchase>
) {
val success: Boolean
get() = billingResult.responseCode == BillingResponseCode.OK
val responseCodeString: String
get() = billingResult.responseCodeString
val purchases: List<com.android.billingclient.api.Purchase>
get() = purchasesList
}
companion object { companion object {
const val TYPE_SUBS = SkuType.SUBS const val TYPE_SUBS = ProductType.SUBS
const val STATE_PURCHASED = PurchaseState.PURCHASED const val STATE_PURCHASED = PurchaseState.PURCHASED
private val PurchasesResult.success: Boolean
get() = billingResult.responseCode == BillingResponseCode.OK
private val BillingResult.success: Boolean private val BillingResult.success: Boolean
get() = responseCode == BillingResponseCode.OK get() = responseCode == BillingResponseCode.OK
@ -251,11 +314,5 @@ class BillingClientImpl(
PurchaseState.PENDING -> "PENDING" PurchaseState.PENDING -> "PENDING"
else -> this.toString() else -> this.toString()
} }
private val PurchasesResult.responseCodeString: String
get() = billingResult.responseCodeString
private val PurchasesResult.purchases: List<com.android.billingclient.api.Purchase>
get() = purchasesList
} }
} }

@ -31,7 +31,7 @@ class Purchase(private val purchase: Purchase) {
get() = purchase.signature get() = purchase.signature
val sku: String val sku: String
get() = purchase.skus.first() get() = purchase.products.first()
val purchaseToken: String val purchaseToken: String
get() = purchase.purchaseToken get() = purchase.purchaseToken
@ -55,7 +55,7 @@ class Purchase(private val purchase: Purchase) {
get() { get() {
val matcher = PATTERN.matcher(sku) val matcher = PATTERN.matcher(sku)
if (matcher.matches()) { if (matcher.matches()) {
val price = matcher.group(2).toInt() val price = matcher.group(2)?.toInt()
return if (price == 499) 5 else price return if (price == 499) 5 else price
} }
return null return null

@ -632,16 +632,16 @@
+| +--- com.google.android.gms:play-services-base:18.5.0 -> 18.8.0 (*) +| +--- com.google.android.gms:play-services-base:18.5.0 -> 18.8.0 (*)
+| +--- com.google.android.gms:play-services-basement:18.4.0 -> 18.8.0 (*) +| +--- com.google.android.gms:play-services-basement:18.4.0 -> 18.8.0 (*)
+| \--- com.google.android.gms:play-services-tasks:18.2.0 -> 18.4.0 (*) +| \--- com.google.android.gms:play-services-tasks:18.2.0 -> 18.4.0 (*)
++--- com.android.billingclient:billing-ktx:6.2.1 ++--- com.android.billingclient:billing-ktx:7.1.1
+| +--- com.android.billingclient:billing:6.2.1 +| +--- com.android.billingclient:billing:7.1.1
+| | +--- androidx.activity:activity:1.2.3 -> 1.11.0 (*) +| | +--- androidx.activity:activity:1.2.3 -> 1.11.0 (*)
+| | +--- com.google.android.datatransport:transport-api:3.0.0 -> 3.2.0 (*) +| | +--- com.google.android.datatransport:transport-api:3.0.0 -> 3.2.0 (*)
+| | +--- com.google.android.datatransport:transport-backend-cct:3.1.8 -> 3.3.0 (*) +| | +--- com.google.android.datatransport:transport-backend-cct:3.1.8 -> 3.3.0 (*)
+| | +--- com.google.android.datatransport:transport-runtime:3.1.8 -> 3.3.0 (*) +| | +--- com.google.android.datatransport:transport-runtime:3.1.8 -> 3.3.0 (*)
+| | +--- com.google.android.gms:play-services-base:18.3.0 -> 18.8.0 (*) +| | +--- com.google.android.gms:play-services-base:18.5.0 -> 18.8.0 (*)
+| | +--- com.google.android.gms:play-services-basement:18.3.0 -> 18.8.0 (*) +| | +--- com.google.android.gms:play-services-basement:18.4.0 -> 18.8.0 (*)
+| | +--- com.google.android.gms:play-services-location:19.0.0 -> 21.3.0 (*) +| | +--- com.google.android.gms:play-services-location:19.0.0 -> 21.3.0 (*)
+| | \--- com.google.android.gms:play-services-tasks:18.1.0 -> 18.4.0 (*) +| | \--- com.google.android.gms:play-services-tasks:18.2.0 -> 18.4.0 (*)
+| +--- org.jetbrains.kotlin:kotlin-stdlib:1.4.0 -> 2.2.0 (*) +| +--- org.jetbrains.kotlin:kotlin-stdlib:1.4.0 -> 2.2.0 (*)
+| \--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0 -> 1.10.2 (*) +| \--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0 -> 1.10.2 (*)
++--- com.google.android.play:review-ktx:2.0.2 ++--- com.google.android.play:review-ktx:2.0.2

@ -167,7 +167,7 @@ okhttp-mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.
osmdroid = { module = "org.osmdroid:osmdroid-android", version.ref = "osmdroid" } osmdroid = { module = "org.osmdroid:osmdroid-android", version.ref = "osmdroid" }
oss-licenses-plugin = { module = "com.google.android.gms:oss-licenses-plugin", version.ref = "oss-licenses-plugin" } oss-licenses-plugin = { module = "com.google.android.gms:oss-licenses-plugin", version.ref = "oss-licenses-plugin" }
persistent-cookiejar = { module = "com.github.franmontiel:PersistentCookieJar", version.ref = "persistent-cookiejar" } persistent-cookiejar = { module = "com.github.franmontiel:PersistentCookieJar", version.ref = "persistent-cookiejar" }
play-billing-ktx = { module = "com.android.billingclient:billing-ktx", version = "6.2.1" } play-billing-ktx = { module = "com.android.billingclient:billing-ktx", version = "7.1.1" }
play-review = { module = "com.google.android.play:review-ktx", version = "2.0.2" } play-review = { module = "com.google.android.play:review-ktx", version = "2.0.2" }
play-services-maps = { module = "com.google.android.gms:play-services-maps", version.ref = "play-services-maps" } play-services-maps = { module = "com.google.android.gms:play-services-maps", version.ref = "play-services-maps" }
play-services-location = { module = "com.google.android.gms:play-services-location", version.ref = "play-services-location" } play-services-location = { module = "com.google.android.gms:play-services-location", version.ref = "play-services-location" }

Loading…
Cancel
Save