mirror of
https://github.com/anyproto/anytype-kotlin.git
synced 2025-06-08 05:47:05 +09:00
DROID-2575 Membership | Billing purchases logic (#1274)
This commit is contained in:
parent
91acfe5a66
commit
6a9644ee11
8 changed files with 78 additions and 64 deletions
|
@ -1,7 +1,6 @@
|
|||
plugins {
|
||||
id "com.android.library"
|
||||
id "kotlin-android"
|
||||
id "kotlinx-serialization"
|
||||
}
|
||||
|
||||
android {
|
||||
|
@ -48,8 +47,6 @@ dependencies {
|
|||
|
||||
implementation libs.timber
|
||||
|
||||
implementation libs.kotlinxSerializationJson
|
||||
|
||||
testImplementation libs.junit
|
||||
testImplementation libs.kotlinTest
|
||||
testImplementation libs.mockitoKotlin
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
package com.anytypeio.anytype.payments.mapping
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import com.android.billingclient.api.ProductDetails
|
||||
import com.android.billingclient.api.Purchase
|
||||
import com.anytypeio.anytype.core_models.membership.Membership
|
||||
import com.anytypeio.anytype.core_models.membership.MembershipPaymentMethod
|
||||
import com.anytypeio.anytype.core_models.membership.MembershipPaymentMethod.METHOD_CRYPTO
|
||||
|
@ -22,6 +19,7 @@ import com.anytypeio.anytype.payments.constants.MembershipConstants.MEMBERSHIP_L
|
|||
import com.anytypeio.anytype.payments.constants.MembershipConstants.PRIVACY_POLICY
|
||||
import com.anytypeio.anytype.payments.constants.MembershipConstants.TERMS_OF_SERVICE
|
||||
import com.anytypeio.anytype.payments.models.BillingPriceInfo
|
||||
import com.anytypeio.anytype.payments.models.MembershipPurchase
|
||||
import com.anytypeio.anytype.payments.models.Tier
|
||||
import com.anytypeio.anytype.payments.models.TierAnyName
|
||||
import com.anytypeio.anytype.payments.models.TierButton
|
||||
|
@ -218,8 +216,12 @@ private fun MembershipTierData.mapActiveTierButtonAndNameStates(
|
|||
0 -> TierButton.Manage.Android.Disabled to TierAnyName.Hidden
|
||||
1 -> {
|
||||
val purchase = purchases[0]
|
||||
val purchaseModel = Json.decodeFromString<PurchaseModel>(purchase.originalJson)
|
||||
if (purchaseModel.obfuscatedAccountId == accountId && purchaseModel.productId == androidProductId) {
|
||||
val purchaseObfuscatedAccountId = purchase.accountId
|
||||
val containsProduct = purchase.products.any { it == androidProductId }
|
||||
if (purchaseObfuscatedAccountId == accountId
|
||||
&& containsProduct
|
||||
&& purchase.state == MembershipPurchase.PurchaseState.PURCHASED
|
||||
) {
|
||||
TierButton.Manage.Android.Enabled(androidProductId) to TierAnyName.Hidden
|
||||
} else {
|
||||
TierButton.Manage.Android.Disabled to TierAnyName.Hidden
|
||||
|
@ -294,17 +296,18 @@ private fun handleNoPurchasesState(
|
|||
private fun getButtonStateAccordingToPurchaseState(
|
||||
androidProductId: String?,
|
||||
accountId: String,
|
||||
purchases: List<Purchase>
|
||||
purchases: List<MembershipPurchase>
|
||||
): TierButton {
|
||||
return when (purchases.size) {
|
||||
0 -> TierButton.Hidden
|
||||
1 -> {
|
||||
val purchase = purchases[0]
|
||||
val purchaseModel = Json.decodeFromString<PurchaseModel>(purchase.originalJson)
|
||||
val purchaseAccountId = purchase.accountId
|
||||
val containsProduct = purchase.products.any { it == androidProductId }
|
||||
when {
|
||||
purchaseModel.obfuscatedAccountId != accountId ->
|
||||
purchaseAccountId != accountId ->
|
||||
TierButton.HiddenWithText.DifferentPurchaseAccountId
|
||||
purchaseModel.productId != androidProductId ->
|
||||
!containsProduct ->
|
||||
TierButton.HiddenWithText.DifferentPurchaseProductId
|
||||
else -> TierButton.Hidden
|
||||
}
|
||||
|
@ -443,8 +446,3 @@ private fun MembershipTierData.getTierEmail(isActive: Boolean, membershipEmail:
|
|||
return TierEmail.Hidden
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class PurchaseModel(
|
||||
val obfuscatedAccountId: String,
|
||||
val productId: String
|
||||
)
|
|
@ -0,0 +1,34 @@
|
|||
package com.anytypeio.anytype.payments.models
|
||||
|
||||
import com.android.billingclient.api.Purchase
|
||||
import timber.log.Timber
|
||||
|
||||
data class MembershipPurchase(
|
||||
val accountId: String,
|
||||
val products: List<String>,
|
||||
val state: PurchaseState
|
||||
) {
|
||||
|
||||
enum class PurchaseState {
|
||||
UNSPECIFIED_STATE,
|
||||
PURCHASED,
|
||||
PENDING
|
||||
}
|
||||
}
|
||||
|
||||
fun Purchase.toMembershipPurchase(): MembershipPurchase? {
|
||||
val obfuscatedAccountId = accountIdentifiers?.obfuscatedAccountId
|
||||
if (obfuscatedAccountId == null) {
|
||||
Timber.e("Billing purchase does not have obfuscatedAccountId")
|
||||
return null
|
||||
}
|
||||
return MembershipPurchase(
|
||||
accountId = obfuscatedAccountId,
|
||||
products = products,
|
||||
state = when (purchaseState) {
|
||||
Purchase.PurchaseState.PURCHASED -> MembershipPurchase.PurchaseState.PURCHASED
|
||||
Purchase.PurchaseState.PENDING -> MembershipPurchase.PurchaseState.PENDING
|
||||
else -> MembershipPurchase.PurchaseState.UNSPECIFIED_STATE
|
||||
}
|
||||
)
|
||||
}
|
|
@ -18,6 +18,8 @@ import com.android.billingclient.api.PurchasesUpdatedListener
|
|||
import com.android.billingclient.api.QueryProductDetailsParams
|
||||
import com.android.billingclient.api.QueryPurchasesParams
|
||||
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
|
||||
import com.anytypeio.anytype.payments.models.MembershipPurchase
|
||||
import com.anytypeio.anytype.payments.models.toMembershipPurchase
|
||||
import kotlin.math.min
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
@ -330,12 +332,17 @@ class BillingClientLifecycle(
|
|||
_subscriptionPurchases.emit(BillingPurchaseState.NoPurchases)
|
||||
} else {
|
||||
Timber.d("processPurchases: Subscription purchases found ${subscriptionPurchaseList[0]}")
|
||||
_subscriptionPurchases.emit(
|
||||
BillingPurchaseState.HasPurchases(
|
||||
purchases = subscriptionPurchaseList,
|
||||
isNewPurchase = isNewPurchase
|
||||
val membershipPurchases = subscriptionPurchaseList.mapNotNull { it.toMembershipPurchase() }
|
||||
if (membershipPurchases.isNotEmpty()) {
|
||||
_subscriptionPurchases.emit(
|
||||
BillingPurchaseState.HasPurchases(
|
||||
purchases = membershipPurchases,
|
||||
isNewPurchase = isNewPurchase
|
||||
)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
_subscriptionPurchases.emit(BillingPurchaseState.NoPurchases)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -386,6 +393,9 @@ sealed class BillingClientState {
|
|||
|
||||
sealed class BillingPurchaseState {
|
||||
data object Loading : BillingPurchaseState()
|
||||
data class HasPurchases(val purchases: List<Purchase>, val isNewPurchase: Boolean) : BillingPurchaseState()
|
||||
data class HasPurchases(
|
||||
val purchases: List<MembershipPurchase>,
|
||||
val isNewPurchase: Boolean
|
||||
) : BillingPurchaseState()
|
||||
data object NoPurchases : BillingPurchaseState()
|
||||
}
|
|
@ -8,7 +8,6 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import com.android.billingclient.api.BillingFlowParams
|
||||
import com.android.billingclient.api.ProductDetails
|
||||
import com.android.billingclient.api.Purchase
|
||||
import com.anytypeio.anytype.analytics.base.Analytics
|
||||
import com.anytypeio.anytype.core_models.membership.EmailVerificationStatus
|
||||
import com.anytypeio.anytype.core_models.membership.MembershipErrors
|
||||
|
@ -23,6 +22,7 @@ import com.anytypeio.anytype.domain.payments.VerifyMembershipEmailCode
|
|||
import com.anytypeio.anytype.payments.constants.MembershipConstants.EXPLORER_ID
|
||||
import com.anytypeio.anytype.payments.constants.MembershipConstants.MEMBERSHIP_NAME_MIN_LENGTH
|
||||
import com.anytypeio.anytype.payments.mapping.toMainView
|
||||
import com.anytypeio.anytype.payments.models.MembershipPurchase
|
||||
import com.anytypeio.anytype.payments.models.TierAnyName
|
||||
import com.anytypeio.anytype.payments.models.TierButton
|
||||
import com.anytypeio.anytype.payments.models.TierEmail
|
||||
|
@ -704,13 +704,13 @@ class MembershipViewModel(
|
|||
* even if the Google account on this device has purchased the subscription.
|
||||
* In this example, the method will return true.
|
||||
*/
|
||||
private fun deviceHasGooglePlaySubscription(purchases: List<Purchase>?, product: String) =
|
||||
private fun deviceHasGooglePlaySubscription(purchases: List<MembershipPurchase>?, product: String) =
|
||||
purchaseForProduct(purchases, product) != null
|
||||
|
||||
/**
|
||||
* Return purchase for the provided Product, if it exists.
|
||||
*/
|
||||
private fun purchaseForProduct(purchases: List<Purchase>?, product: String): Purchase? {
|
||||
private fun purchaseForProduct(purchases: List<MembershipPurchase>?, product: String): MembershipPurchase? {
|
||||
purchases?.let {
|
||||
for (purchase in it) {
|
||||
if (purchase.products[0] == product) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.anytypeio.anytype.payments
|
||||
|
||||
import app.cash.turbine.turbineScope
|
||||
import com.android.billingclient.api.AccountIdentifiers
|
||||
import com.android.billingclient.api.ProductDetails
|
||||
import com.android.billingclient.api.Purchase
|
||||
import com.anytypeio.anytype.core_models.membership.Membership
|
||||
|
@ -9,6 +10,7 @@ import com.anytypeio.anytype.core_models.membership.MembershipPeriodType
|
|||
import com.anytypeio.anytype.core_models.membership.MembershipTierData
|
||||
import com.anytypeio.anytype.payments.constants.MembershipConstants
|
||||
import com.anytypeio.anytype.payments.models.BillingPriceInfo
|
||||
import com.anytypeio.anytype.payments.models.MembershipPurchase
|
||||
import com.anytypeio.anytype.payments.models.PeriodDescription
|
||||
import com.anytypeio.anytype.payments.models.PeriodUnit
|
||||
import com.anytypeio.anytype.payments.models.TierAnyName
|
||||
|
@ -22,6 +24,7 @@ import com.anytypeio.anytype.payments.viewmodel.MembershipMainState
|
|||
import com.anytypeio.anytype.payments.viewmodel.MembershipTierState
|
||||
import com.anytypeio.anytype.presentation.membership.models.MembershipStatus
|
||||
import com.anytypeio.anytype.presentation.membership.models.TierId
|
||||
import java.lang.reflect.Member
|
||||
import kotlin.test.assertIs
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
@ -32,6 +35,7 @@ import org.junit.Test
|
|||
import org.mockito.Mockito
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.stub
|
||||
import org.mockito.kotlin.whenever
|
||||
|
||||
class TierActiveWithDifferentSubIdTest : MembershipTestsSetup() {
|
||||
|
||||
|
@ -98,12 +102,7 @@ class TierActiveWithDifferentSubIdTest : MembershipTestsSetup() {
|
|||
stubBilling(billingClientState = BillingClientState.Connected(listOf(product1)))
|
||||
|
||||
// Mocking purchase
|
||||
val purchase = Mockito.mock(Purchase::class.java)
|
||||
Mockito.`when`(purchase.products).thenReturn(listOf(androidProductId))
|
||||
Mockito.`when`(purchase.isAcknowledged).thenReturn(true)
|
||||
val purchaseJson =
|
||||
"{\"obfuscatedAccountId\":\"$accountIdDifferent\", \"productId\":\"$androidProductId\"}"
|
||||
Mockito.`when`(purchase.originalJson).thenReturn(purchaseJson)
|
||||
val purchase = MembershipPurchase(accountIdDifferent, listOf(androidProductId), MembershipPurchase.PurchaseState.PURCHASED)
|
||||
stubPurchaseState(BillingPurchaseState.HasPurchases(listOf(purchase), false))
|
||||
|
||||
// Mocking the flow of membership status
|
||||
|
@ -224,12 +223,7 @@ class TierActiveWithDifferentSubIdTest : MembershipTestsSetup() {
|
|||
)
|
||||
)
|
||||
|
||||
val purchase = Mockito.mock(Purchase::class.java)
|
||||
Mockito.`when`(purchase.products).thenReturn(listOf(invalidProductId))
|
||||
Mockito.`when`(purchase.isAcknowledged).thenReturn(true)
|
||||
val purchaseJson =
|
||||
"{\"obfuscatedAccountId\":\"$accountId\", \"productId\":\"$invalidProductId\"}"
|
||||
Mockito.`when`(purchase.originalJson).thenReturn(purchaseJson)
|
||||
val purchase = MembershipPurchase(accountId, listOf(invalidProductId), MembershipPurchase.PurchaseState.PURCHASED)
|
||||
stubPurchaseState(BillingPurchaseState.HasPurchases(listOf(purchase), false))
|
||||
|
||||
val flow = flow {
|
||||
|
@ -431,20 +425,8 @@ class TierActiveWithDifferentSubIdTest : MembershipTestsSetup() {
|
|||
)
|
||||
)
|
||||
|
||||
val purchase1 = Mockito.mock(Purchase::class.java)
|
||||
Mockito.`when`(purchase1.products).thenReturn(listOf(invalidProductId))
|
||||
Mockito.`when`(purchase1.isAcknowledged).thenReturn(true)
|
||||
val purchaseJson1 =
|
||||
"{\"obfuscatedAccountId\":\"$accountId\", \"productId\":\"$invalidProductId\"}"
|
||||
Mockito.`when`(purchase1.originalJson).thenReturn(purchaseJson1)
|
||||
|
||||
val purchase2 = Mockito.mock(Purchase::class.java)
|
||||
Mockito.`when`(purchase2.products).thenReturn(listOf(invalidProductId))
|
||||
Mockito.`when`(purchase2.isAcknowledged).thenReturn(true)
|
||||
val purchaseJson2 =
|
||||
"{\"obfuscatedAccountId\":\"$accountId\", \"productId\":\"$androidProductId\"}"
|
||||
Mockito.`when`(purchase2.originalJson).thenReturn(purchaseJson2)
|
||||
|
||||
val purchase1 = MembershipPurchase(accountId, listOf(invalidProductId), MembershipPurchase.PurchaseState.PURCHASED)
|
||||
val purchase2 = MembershipPurchase(accountId, listOf(androidProductId), MembershipPurchase.PurchaseState.PURCHASED)
|
||||
stubPurchaseState(BillingPurchaseState.HasPurchases(listOf(purchase1, purchase2), false))
|
||||
|
||||
val flow = flow {
|
||||
|
|
|
@ -7,6 +7,7 @@ import com.anytypeio.anytype.core_models.membership.MembershipPaymentMethod
|
|||
import com.anytypeio.anytype.core_models.membership.MembershipPeriodType
|
||||
import com.anytypeio.anytype.core_models.membership.MembershipTierData
|
||||
import com.anytypeio.anytype.payments.constants.MembershipConstants
|
||||
import com.anytypeio.anytype.payments.models.MembershipPurchase
|
||||
import com.anytypeio.anytype.payments.models.TierAnyName
|
||||
import com.anytypeio.anytype.payments.models.TierButton
|
||||
import com.anytypeio.anytype.payments.models.TierConditionInfo
|
||||
|
@ -208,12 +209,7 @@ class TierAndroidActiveTests : MembershipTestsSetup() {
|
|||
val (features, tiers) = commonTestSetup()
|
||||
|
||||
stubBilling()
|
||||
val purchase = Mockito.mock(Purchase::class.java)
|
||||
Mockito.`when`(purchase.products).thenReturn(listOf(androidProductId))
|
||||
Mockito.`when`(purchase.isAcknowledged).thenReturn(true)
|
||||
val purchaseJson =
|
||||
"{\"obfuscatedAccountId\":\"$accountId\", \"productId\":\"$androidProductId\"}"
|
||||
Mockito.`when`(purchase.originalJson).thenReturn(purchaseJson)
|
||||
val purchase = MembershipPurchase(accountId, listOf(androidProductId), MembershipPurchase.PurchaseState.PURCHASED)
|
||||
stubPurchaseState(BillingPurchaseState.HasPurchases(listOf(purchase), false))
|
||||
stubMembershipProvider(setupMembershipStatus(tiers))
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import com.anytypeio.anytype.core_models.membership.MembershipTierData
|
|||
import com.anytypeio.anytype.payments.constants.MembershipConstants.BUILDER_ID
|
||||
import com.anytypeio.anytype.payments.constants.MembershipConstants.EXPLORER_ID
|
||||
import com.anytypeio.anytype.payments.models.BillingPriceInfo
|
||||
import com.anytypeio.anytype.payments.models.MembershipPurchase
|
||||
import com.anytypeio.anytype.payments.models.PeriodDescription
|
||||
import com.anytypeio.anytype.payments.models.PeriodUnit
|
||||
import com.anytypeio.anytype.payments.models.TierAnyName
|
||||
|
@ -84,12 +85,8 @@ class TierBuilderFallbackOnExplorerTest : MembershipTestsSetup() {
|
|||
Mockito.`when`(pricingPhaseList[0]?.billingPeriod).thenReturn("P1Y")
|
||||
stubBilling(billingClientState = BillingClientState.Connected(listOf(product)))
|
||||
|
||||
val purchase = Mockito.mock(Purchase::class.java)
|
||||
Mockito.`when`(purchase.products).thenReturn(listOf(androidProductId))
|
||||
Mockito.`when`(purchase.isAcknowledged).thenReturn(true)
|
||||
val purchaseJson =
|
||||
"{\"obfuscatedAccountId\":\"$accountId\", \"productId\":\"$androidProductId\"}"
|
||||
Mockito.`when`(purchase.originalJson).thenReturn(purchaseJson)
|
||||
|
||||
val purchase = MembershipPurchase(accountId, listOf(androidProductId), MembershipPurchase.PurchaseState.PURCHASED)
|
||||
stubPurchaseState(BillingPurchaseState.HasPurchases(listOf(purchase), false))
|
||||
val flow = flow {
|
||||
emit(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue