deps(Android): Update to Android Billing v6.0
This commit is contained in:
parent
0ca1a1c15e
commit
be9807c5af
|
@ -4,9 +4,10 @@ import android.app.Activity
|
|||
import com.android.billingclient.api.AcknowledgePurchaseParams
|
||||
import com.android.billingclient.api.BillingClient
|
||||
import com.android.billingclient.api.BillingFlowParams
|
||||
import com.android.billingclient.api.ProductDetails
|
||||
import com.android.billingclient.api.Purchase
|
||||
import com.android.billingclient.api.SkuDetails
|
||||
import com.android.billingclient.api.SkuDetailsParams
|
||||
import com.android.billingclient.api.QueryProductDetailsParams
|
||||
import com.android.billingclient.api.QueryPurchasesParams
|
||||
import com.artemchep.keyguard.common.model.RichResult
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
@ -21,7 +22,7 @@ interface BillingConnection {
|
|||
* Perform a network query to get SKU details and
|
||||
* return the result asynchronously.
|
||||
*/
|
||||
fun skuDetailsFlow(skuDetailsParams: SkuDetailsParams): Flow<RichResult<List<SkuDetails>>>
|
||||
fun productDetailsFlow(params: QueryProductDetailsParams): Flow<RichResult<List<ProductDetails>>>
|
||||
|
||||
/**
|
||||
* Get purchases details for all the items bought within your app.
|
||||
|
@ -32,7 +33,7 @@ interface BillingConnection {
|
|||
* https://developers.google.com/android-publisher/api-ref/purchases/products/get
|
||||
* https://developers.google.com/android-publisher/api-ref/purchases/subscriptions/get
|
||||
*/
|
||||
fun purchasesFlow(skuType: String): Flow<RichResult<List<Purchase>>>
|
||||
fun purchasesFlow(params: QueryPurchasesParams): Flow<RichResult<List<Purchase>>>
|
||||
|
||||
fun launchBillingFlow(activity: Activity, billingFlowParams: BillingFlowParams)
|
||||
|
||||
|
|
|
@ -8,7 +8,10 @@ import com.android.billingclient.api.AcknowledgePurchaseParams
|
|||
import com.android.billingclient.api.BillingClient
|
||||
import com.android.billingclient.api.BillingFlowParams
|
||||
import com.android.billingclient.api.BillingResult
|
||||
import com.android.billingclient.api.ProductDetails
|
||||
import com.android.billingclient.api.Purchase
|
||||
import com.android.billingclient.api.QueryProductDetailsParams
|
||||
import com.android.billingclient.api.QueryPurchasesParams
|
||||
import com.android.billingclient.api.SkuDetails
|
||||
import com.android.billingclient.api.SkuDetailsParams
|
||||
import com.artemchep.keyguard.common.io.IO
|
||||
|
@ -59,14 +62,14 @@ class BillingConnectionImpl(
|
|||
private const val TIMEOUT_LAUNCH_BILLING_FLOW = 1000L
|
||||
}
|
||||
|
||||
override fun skuDetailsFlow(skuDetailsParams: SkuDetailsParams): Flow<RichResult<List<SkuDetails>>> =
|
||||
override fun productDetailsFlow(params: QueryProductDetailsParams): Flow<RichResult<List<ProductDetails>>> =
|
||||
mapClient { client ->
|
||||
client.mapToSkuDetails(skuDetailsParams)
|
||||
client.mapToSkuDetails(params)
|
||||
}
|
||||
|
||||
private suspend fun BillingClient.mapToSkuDetails(skuDetailsParams: SkuDetailsParams) =
|
||||
private suspend fun BillingClient.mapToSkuDetails(params: QueryProductDetailsParams) =
|
||||
ioEffect(Dispatchers.IO) {
|
||||
querySkuDetailsSuspending(skuDetailsParams)
|
||||
querySkuDetailsSuspending(params)
|
||||
}
|
||||
.flattenMap()
|
||||
.flatMap { skuDetailsList ->
|
||||
|
@ -76,16 +79,16 @@ class BillingConnectionImpl(
|
|||
?: kotlin.run {
|
||||
val exception =
|
||||
IllegalArgumentException("Sku details list must not be null!")
|
||||
ioRaise<List<SkuDetails>>(exception)
|
||||
ioRaise<List<ProductDetails>>(exception)
|
||||
}
|
||||
}
|
||||
.retryIfNetworkIssue()
|
||||
.attempt().bind().let { RichResult.invoke(it) }
|
||||
|
||||
override fun purchasesFlow(skuType: String): Flow<RichResult<List<Purchase>>> =
|
||||
override fun purchasesFlow(params: QueryPurchasesParams): Flow<RichResult<List<Purchase>>> =
|
||||
mapClient { client ->
|
||||
ioEffect(Dispatchers.IO) {
|
||||
client.queryPurchasesSuspending(skuType)
|
||||
client.queryPurchasesSuspending(params)
|
||||
}
|
||||
.flattenMap()
|
||||
.flatMap { purchasesList ->
|
||||
|
@ -169,9 +172,9 @@ class BillingConnectionImpl(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun BillingClient.querySkuDetailsSuspending(skuDetailsParams: SkuDetailsParams) =
|
||||
suspendCancellableCoroutine<Either<BillingResponseException, List<SkuDetails>?>> { continuation ->
|
||||
querySkuDetailsAsync(skuDetailsParams) { billingResult, skuDetailsList ->
|
||||
private suspend fun BillingClient.querySkuDetailsSuspending(params: QueryProductDetailsParams) =
|
||||
suspendCancellableCoroutine<Either<BillingResponseException, List<ProductDetails>?>> { continuation ->
|
||||
queryProductDetailsAsync(params) { billingResult, skuDetailsList ->
|
||||
kotlin.runCatching {
|
||||
val r = getBillingResultOrException(billingResult, skuDetailsList)
|
||||
continuation.resume(r)
|
||||
|
@ -179,9 +182,9 @@ private suspend fun BillingClient.querySkuDetailsSuspending(skuDetailsParams: Sk
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun BillingClient.queryPurchasesSuspending(skuType: String) =
|
||||
private suspend fun BillingClient.queryPurchasesSuspending(params: QueryPurchasesParams) =
|
||||
suspendCancellableCoroutine<Either<BillingResponseException, List<Purchase>?>> { continuation ->
|
||||
queryPurchasesAsync(skuType) { billingResult, purchases ->
|
||||
queryPurchasesAsync(params) { billingResult, purchases ->
|
||||
kotlin.runCatching {
|
||||
val r = getBillingResultOrException(billingResult, purchases)
|
||||
continuation.resume(r)
|
||||
|
|
|
@ -2,10 +2,11 @@ package com.artemchep.keyguard.copy
|
|||
|
||||
import android.app.Activity
|
||||
import com.android.billingclient.api.AcknowledgePurchaseParams
|
||||
import com.android.billingclient.api.BillingClient.SkuType
|
||||
import com.android.billingclient.api.BillingClient.ProductType
|
||||
import com.android.billingclient.api.BillingFlowParams
|
||||
import com.android.billingclient.api.Purchase
|
||||
import com.android.billingclient.api.SkuDetailsParams
|
||||
import com.android.billingclient.api.QueryProductDetailsParams
|
||||
import com.android.billingclient.api.QueryPurchasesParams
|
||||
import com.artemchep.keyguard.android.closestActivityOrNull
|
||||
import com.artemchep.keyguard.billing.BillingConnection
|
||||
import com.artemchep.keyguard.billing.BillingManager
|
||||
|
@ -59,8 +60,8 @@ class SubscriptionServiceAndroid(
|
|||
.map { receiptsResult ->
|
||||
receiptsResult.map { receipts ->
|
||||
receipts.any {
|
||||
val isSubscription = it.skus.intersect(SkuListSubscription).isNotEmpty()
|
||||
val isProduct = it.skus.intersect(SkuListProduct).isNotEmpty()
|
||||
val isSubscription = it.products.intersect(SkuListSubscription).isNotEmpty()
|
||||
val isProduct = it.products.intersect(SkuListProduct).isNotEmpty()
|
||||
isSubscription || isProduct
|
||||
}
|
||||
}
|
||||
|
@ -69,47 +70,80 @@ class SubscriptionServiceAndroid(
|
|||
override fun subscriptions(): Flow<List<Subscription>?> = combine(
|
||||
getReceiptFlow()
|
||||
.filter { it !is RichResult.Loading },
|
||||
getSkuDetailsFlow(SkuType.SUBS)
|
||||
getProductDetailsFlow(ProductType.SUBS)
|
||||
.filter { it !is RichResult.Loading },
|
||||
) { receiptsResult, skuDetailsResult ->
|
||||
val receipts = receiptsResult.orNull()
|
||||
val skuDetails = skuDetailsResult.orNull()
|
||||
|
||||
skuDetails?.map {
|
||||
skuDetails?.mapNotNull {
|
||||
val bestOffer = it.subscriptionOfferDetails
|
||||
?.filter { it.offerId != null }
|
||||
?.minByOrNull {
|
||||
val firstPrice = it.pricingPhases
|
||||
.pricingPhaseList
|
||||
.first()
|
||||
firstPrice.priceAmountMicros
|
||||
}
|
||||
val baseOffer = it.subscriptionOfferDetails
|
||||
?.firstOrNull { it.offerId == null }
|
||||
?: return@mapNotNull null
|
||||
val finalPrice = baseOffer.pricingPhases
|
||||
.pricingPhaseList
|
||||
.last()
|
||||
|
||||
val status = kotlin.run {
|
||||
val skuReceipt = receipts?.firstOrNull { purchase -> it.sku in purchase.skus }
|
||||
?: return@run Subscription.Status.Inactive
|
||||
val skuReceipt =
|
||||
receipts?.firstOrNull { purchase -> it.productId in purchase.products }
|
||||
?: return@run kotlin.run {
|
||||
val hasTrialAvailable = bestOffer?.pricingPhases
|
||||
?.pricingPhaseList
|
||||
?.firstOrNull()
|
||||
?.priceAmountMicros == 0L
|
||||
Subscription.Status.Inactive(
|
||||
hasTrialAvailable = hasTrialAvailable,
|
||||
)
|
||||
}
|
||||
Subscription.Status.Active(
|
||||
willRenew = skuReceipt.isAutoRenewing,
|
||||
)
|
||||
}
|
||||
Subscription(
|
||||
id = it.sku,
|
||||
title = it.title,
|
||||
id = it.productId,
|
||||
title = it.name,
|
||||
description = it.description,
|
||||
price = it.price,
|
||||
price = finalPrice.formattedPrice,
|
||||
status = status,
|
||||
purchase = { context ->
|
||||
val activity = context.context.closestActivityOrNull
|
||||
?: return@Subscription
|
||||
|
||||
val offerToken = bestOffer?.offerToken
|
||||
?: baseOffer.offerToken
|
||||
val list = listOf(
|
||||
BillingFlowParams.ProductDetailsParams.newBuilder()
|
||||
.setProductDetails(it)
|
||||
.setOfferToken(offerToken)
|
||||
.build()
|
||||
)
|
||||
val params = BillingFlowParams.newBuilder()
|
||||
.run {
|
||||
val existingPurchase = receipts
|
||||
?.firstOrNull {
|
||||
SkuListSubscription.intersect(it.skus)
|
||||
SkuListSubscription.intersect(it.products)
|
||||
.isNotEmpty()
|
||||
}
|
||||
if (existingPurchase != null && it.sku !in existingPurchase.skus) {
|
||||
if (existingPurchase != null && it.productId !in existingPurchase.products) {
|
||||
val params = BillingFlowParams.SubscriptionUpdateParams
|
||||
.newBuilder()
|
||||
.setOldSkuPurchaseToken(existingPurchase.purchaseToken)
|
||||
.setOldPurchaseToken(existingPurchase.purchaseToken)
|
||||
.build()
|
||||
setSubscriptionUpdateParams(params)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
.setSkuDetails(it)
|
||||
.setProductDetailsParamsList(list)
|
||||
.build()
|
||||
purchase(activity, params)
|
||||
.crashlyticsTap()
|
||||
|
@ -123,29 +157,37 @@ class SubscriptionServiceAndroid(
|
|||
override fun products(): Flow<List<Product>?> = combine(
|
||||
getReceiptFlow()
|
||||
.filter { it !is RichResult.Loading },
|
||||
getSkuDetailsFlow(SkuType.INAPP)
|
||||
getProductDetailsFlow(ProductType.INAPP)
|
||||
.filter { it !is RichResult.Loading },
|
||||
) { receiptsResult, skuDetailsResult ->
|
||||
val receipts = receiptsResult.orNull()
|
||||
val skuDetails = skuDetailsResult.orNull()
|
||||
|
||||
skuDetails?.map {
|
||||
skuDetails?.mapNotNull {
|
||||
val status = kotlin.run {
|
||||
receipts?.firstOrNull { purchase -> it.sku in purchase.skus }
|
||||
receipts?.firstOrNull { purchase -> it.productId in purchase.products }
|
||||
?: return@run Product.Status.Inactive
|
||||
Product.Status.Active
|
||||
}
|
||||
val finalPrice = it.oneTimePurchaseOfferDetails
|
||||
?: return@mapNotNull null
|
||||
Product(
|
||||
id = it.sku,
|
||||
title = it.title,
|
||||
id = it.productId,
|
||||
title = it.name,
|
||||
description = it.description,
|
||||
price = it.price,
|
||||
price = finalPrice.formattedPrice,
|
||||
status = status,
|
||||
purchase = { context ->
|
||||
val activity = context.context.closestActivityOrNull
|
||||
?: return@Product
|
||||
|
||||
val list = listOf(
|
||||
BillingFlowParams.ProductDetailsParams.newBuilder()
|
||||
.setProductDetails(it)
|
||||
.build()
|
||||
)
|
||||
val params = BillingFlowParams.newBuilder()
|
||||
.setSkuDetails(it)
|
||||
.setProductDetailsParamsList(list)
|
||||
.build()
|
||||
purchase(activity, params)
|
||||
.crashlyticsTap()
|
||||
|
@ -180,10 +222,18 @@ class SubscriptionServiceAndroid(
|
|||
.billingConnectionFlow
|
||||
.flatMapLatest { connection ->
|
||||
val subscriptionsFlow = connection
|
||||
.purchasesFlow(SkuType.SUBS)
|
||||
.purchasesFlow(
|
||||
QueryPurchasesParams.newBuilder()
|
||||
.setProductType(ProductType.SUBS)
|
||||
.build(),
|
||||
)
|
||||
.onEachAcknowledge(connection)
|
||||
val productsFlow = connection
|
||||
.purchasesFlow(SkuType.INAPP)
|
||||
.purchasesFlow(
|
||||
QueryPurchasesParams.newBuilder()
|
||||
.setProductType(ProductType.INAPP)
|
||||
.build(),
|
||||
)
|
||||
.onEachAcknowledge(connection)
|
||||
combine(
|
||||
subscriptionsFlow,
|
||||
|
@ -213,19 +263,24 @@ class SubscriptionServiceAndroid(
|
|||
connection.acknowledgePurchase(acknowledgePurchaseParams)
|
||||
}
|
||||
|
||||
private fun getSkuDetailsFlow(skuType: String) = billingManager
|
||||
private fun getProductDetailsFlow(@ProductType productType: String) = billingManager
|
||||
.billingConnectionFlow
|
||||
.flatMapLatest { connection ->
|
||||
val skusList = when (skuType) {
|
||||
SkuType.SUBS -> SkuListSubscription
|
||||
SkuType.INAPP -> SkuListProduct
|
||||
val productIdList = when (productType) {
|
||||
ProductType.SUBS -> SkuListSubscription
|
||||
ProductType.INAPP -> SkuListProduct
|
||||
else -> error("Unknown SKU type!")
|
||||
}
|
||||
|
||||
val skuDetailsParams = SkuDetailsParams.newBuilder()
|
||||
.setType(skuType)
|
||||
.setSkusList(skusList)
|
||||
val productList = productIdList
|
||||
.map {
|
||||
QueryProductDetailsParams.Product.newBuilder()
|
||||
.setProductId(it)
|
||||
.setProductType(productType)
|
||||
.build()
|
||||
}
|
||||
val skuDetailsParams = QueryProductDetailsParams.newBuilder()
|
||||
.setProductList(productList)
|
||||
.build()
|
||||
connection.skuDetailsFlow(skuDetailsParams)
|
||||
connection.productDetailsFlow(skuDetailsParams)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1077,6 +1077,7 @@
|
|||
<string name="pref_item_premium_membership_failed_to_load_subscriptions">Failed to load a list of subscriptions</string>
|
||||
<string name="pref_item_premium_membership_failed_to_load_products">Failed to load a list of products</string>
|
||||
<string name="pref_item_premium_status_active">Active</string>
|
||||
<string name="pref_item_premium_status_free_trial">Free trial</string>
|
||||
<string name="pref_item_premium_status_will_not_renew">Will not renew</string>
|
||||
<string name="pref_item_premium_manage_subscription_on_play_store_title">Manage on Play Store</string>
|
||||
<string name="pref_item_permission_post_notifications_title">Post notifications</string>
|
||||
|
|
|
@ -11,7 +11,9 @@ data class Subscription(
|
|||
val purchase: (LeContext) -> Unit,
|
||||
) {
|
||||
sealed interface Status {
|
||||
data object Inactive : Status
|
||||
data class Inactive(
|
||||
val hasTrialAvailable: Boolean,
|
||||
) : Status
|
||||
|
||||
data class Active(
|
||||
val willRenew: Boolean,
|
||||
|
|
|
@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
|
@ -35,6 +36,7 @@ import com.artemchep.keyguard.common.model.Subscription
|
|||
import com.artemchep.keyguard.common.model.fold
|
||||
import com.artemchep.keyguard.common.usecase.GetProducts
|
||||
import com.artemchep.keyguard.common.usecase.GetSubscriptions
|
||||
import com.artemchep.keyguard.feature.auth.common.TextFieldModel2
|
||||
import com.artemchep.keyguard.feature.home.vault.component.Section
|
||||
import com.artemchep.keyguard.feature.onboarding.OnboardingCard
|
||||
import com.artemchep.keyguard.feature.onboarding.onboardingItemsPremium
|
||||
|
@ -47,6 +49,7 @@ import com.artemchep.keyguard.ui.ExpandedIfNotEmpty
|
|||
import com.artemchep.keyguard.ui.FlatItemLayout
|
||||
import com.artemchep.keyguard.ui.FlatItemTextContent
|
||||
import com.artemchep.keyguard.ui.FlatSimpleNote
|
||||
import com.artemchep.keyguard.ui.FlatTextFieldBadge
|
||||
import com.artemchep.keyguard.ui.MediumEmphasisAlpha
|
||||
import com.artemchep.keyguard.ui.SimpleNote
|
||||
import com.artemchep.keyguard.ui.icons.ChevronIcon
|
||||
|
@ -268,6 +271,18 @@ private fun SettingSubscriptionItem(
|
|||
},
|
||||
elevation = 2.dp,
|
||||
trailing = {
|
||||
val status = subscription.status
|
||||
if (status is Subscription.Status.Inactive && status.hasTrialAvailable) {
|
||||
FlatTextFieldBadge(
|
||||
type = TextFieldModel2.Vl.Type.INFO,
|
||||
text = stringResource(Res.string.pref_item_premium_status_free_trial),
|
||||
)
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.width(16.dp),
|
||||
)
|
||||
}
|
||||
|
||||
ChevronIcon()
|
||||
},
|
||||
content = {
|
||||
|
|
|
@ -13,7 +13,7 @@ appVersionName = "1.3.1"
|
|||
appVersionCode = "6"
|
||||
# https://github.com/google/accompanist
|
||||
accompanist = "0.34.0"
|
||||
androidBillingClient = "6.2.1"
|
||||
androidBillingClient = "7.0.0"
|
||||
# https://mvnrepository.com/artifact/com.android.tools/desugar_jdk_libs
|
||||
androidDesugar = "2.0.4"
|
||||
# https://mvnrepository.com/artifact/com.android.tools.build/gradle?repo=google
|
||||
|
|
Loading…
Reference in New Issue