1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-08 05:47:05 +09:00

DROID-2277 Membership | Apk builds (#1281)

This commit is contained in:
Konstantin Ivanov 2024-06-10 12:16:31 +02:00 committed by konstantiniiv
parent 64f4d9b8c6
commit a5dec4e5e1
3 changed files with 230 additions and 25 deletions

View file

@ -104,8 +104,7 @@ fun MembershipTierData.toView(
conditionInfo = getConditionInfo(
isActive = isActive,
billingClientState = billingClientState,
membershipStatus = membershipStatus,
billingPurchaseState = billingPurchaseState
membershipStatus = membershipStatus
),
isActive = isActive,
features = features,
@ -138,8 +137,7 @@ fun MembershipTierData.toPreviewView(
conditionInfo = getConditionInfo(
isActive = isActive,
billingClientState = billingClientState,
membershipStatus = membershipStatus,
billingPurchaseState = billingPurchaseState
membershipStatus = membershipStatus
),
isActive = isActive,
color = colorStr
@ -242,6 +240,13 @@ private fun MembershipTierData.mapInactiveTierButtonAndNameStates(
): Pair<TierButton, TierAnyName> {
val androidProductId = this.androidProductId
val androidInfoUrl = this.androidManageUrl
if (billingClientState is BillingClientState.NotAvailable) {
return if (androidInfoUrl == null) {
TierButton.Info.Disabled to TierAnyName.Hidden
} else {
TierButton.Info.Enabled(androidInfoUrl) to TierAnyName.Hidden
}
}
if (androidProductId == null) {
return if (androidInfoUrl == null) {
TierButton.Info.Disabled to TierAnyName.Hidden
@ -269,7 +274,8 @@ private fun MembershipTierData.mapInactiveTierButtonAndNameStates(
handleNoPurchasesState(
billingClientState = billingClientState,
membershipStatus = membershipStatus,
androidProductId = androidProductId
androidProductId = androidProductId,
androidInfoUrl = androidInfoUrl
)
}
}
@ -278,7 +284,8 @@ private fun MembershipTierData.mapInactiveTierButtonAndNameStates(
private fun handleNoPurchasesState(
billingClientState: BillingClientState,
membershipStatus: MembershipStatus,
androidProductId: String
androidProductId: String,
androidInfoUrl: String?
): Pair<TierButton, TierAnyName> {
if (billingClientState is BillingClientState.Connected) {
val product = billingClientState.productDetails.find { it.productId == androidProductId }
@ -289,7 +296,13 @@ private fun handleNoPurchasesState(
else -> TierButton.Pay.Enabled to TierAnyName.Visible.Purchased(membershipStatus.anyName)
}
}
if (billingClientState is BillingClientState.NotAvailable ) {
return if (androidInfoUrl == null) {
TierButton.Info.Disabled to TierAnyName.Hidden
} else {
TierButton.Info.Enabled(androidInfoUrl) to TierAnyName.Hidden
}
}
return TierButton.Pay.Disabled to TierAnyName.Visible.Disabled
}
@ -319,8 +332,7 @@ private fun getButtonStateAccordingToPurchaseState(
private fun MembershipTierData.getConditionInfo(
isActive: Boolean,
billingClientState: BillingClientState,
membershipStatus: MembershipStatus,
billingPurchaseState: BillingPurchaseState
membershipStatus: MembershipStatus
): TierConditionInfo {
return if (isActive) {
createConditionInfoForCurrentTier(
@ -333,8 +345,7 @@ private fun MembershipTierData.getConditionInfo(
} else {
createConditionInfoForBillingTier(
billingClientState = billingClientState,
membershipStatus = membershipStatus,
billingPurchaseState = billingPurchaseState
membershipStatus = membershipStatus
)
}
}
@ -375,8 +386,7 @@ private fun formatPriceInCents(priceInCents: Int): String {
private fun MembershipTierData.createConditionInfoForBillingTier(
billingClientState: BillingClientState,
membershipStatus: MembershipStatus,
billingPurchaseState: BillingPurchaseState
membershipStatus: MembershipStatus
): TierConditionInfo {
if (
membershipStatus.status == Membership.Status.STATUS_PENDING ||
@ -384,9 +394,6 @@ private fun MembershipTierData.createConditionInfoForBillingTier(
) {
return TierConditionInfo.Visible.Pending
}
if (billingPurchaseState is BillingPurchaseState.Loading) {
return TierConditionInfo.Visible.Pending
}
return when (billingClientState) {
BillingClientState.Loading -> {
TierConditionInfo.Visible.LoadingBillingClient
@ -410,6 +417,11 @@ private fun MembershipTierData.createConditionInfoForBillingTier(
}
}
}
BillingClientState.NotAvailable -> {
TierConditionInfo.Visible.Price(
price = formatPriceInCents(priceStripeUsdCents),
period = convertToTierViewPeriod(this))
}
}
}

View file

@ -101,15 +101,22 @@ class BillingClientLifecycle(
val responseCode = billingResult.responseCode
val debugMessage = billingResult.debugMessage
Timber.d("onBillingSetupFinished: $responseCode $debugMessage")
if (responseCode == BillingClient.BillingResponseCode.OK) {
// The billing client is ready.
// You can query product details and purchases here.
querySubscriptionProductDetails()
querySubscriptionPurchases()
} else {
Timber.e("onBillingSetupFinished: BillingResponse $responseCode")
_builderSubProductWithProductDetails.value =
BillingClientState.Error("BillingResponse $responseCode")
when (responseCode) {
BillingClient.BillingResponseCode.OK -> {
// The billing client is ready.
// You can query product details and purchases here.
querySubscriptionProductDetails()
querySubscriptionPurchases()
}
BillingClient.BillingResponseCode.BILLING_UNAVAILABLE -> {
Timber.e("onBillingSetupFinished: BILLING_UNAVAILABLE")
_builderSubProductWithProductDetails.value = BillingClientState.NotAvailable
}
else -> {
Timber.e("onBillingSetupFinished: BillingResponse $responseCode")
_builderSubProductWithProductDetails.value =
BillingClientState.Error("BillingResponse $responseCode")
}
}
}
@ -389,6 +396,7 @@ sealed class BillingClientState {
data class Error(val message: String) : BillingClientState()
//Connected state is suppose that we have non empty list of product details
data class Connected(val productDetails: List<ProductDetails>) : BillingClientState()
data object NotAvailable : BillingClientState()
}
sealed class BillingPurchaseState {

View file

@ -0,0 +1,185 @@
package com.anytypeio.anytype.payments
import app.cash.turbine.turbineScope
import com.anytypeio.anytype.core_models.membership.Membership
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
import com.anytypeio.anytype.payments.models.TierEmail
import com.anytypeio.anytype.payments.models.TierPeriod
import com.anytypeio.anytype.payments.models.TierPreview
import com.anytypeio.anytype.payments.playbilling.BillingClientState
import com.anytypeio.anytype.payments.playbilling.BillingPurchaseState
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 junit.framework.TestCase
import kotlin.test.assertIs
import kotlinx.coroutines.test.runTest
import net.bytebuddy.utility.RandomString
import org.junit.Test
class TierAndroidBillingUnavailableTest : MembershipTestsSetup() {
private fun commonTestSetup(): Pair<List<String>, List<MembershipTierData>> {
val features = listOf("feature-${RandomString.make()}", "feature-${RandomString.make()}")
val tiers = setupTierData(features)
return Pair(features, tiers)
}
private fun setupTierData(features: List<String>): List<MembershipTierData> {
return listOf(
StubMembershipTierData(
id = MembershipConstants.EXPLORER_ID,
androidProductId = null,
features = features,
periodType = MembershipPeriodType.PERIOD_TYPE_UNLIMITED,
priceStripeUsdCents = 0
),
StubMembershipTierData(
id = MembershipConstants.BUILDER_ID,
androidProductId = androidProductId,
features = features,
periodValue = 1,
periodType = MembershipPeriodType.PERIOD_TYPE_YEARS,
priceStripeUsdCents = 9901,
androidManageUrl = "https://anytype.io/pricing"
),
StubMembershipTierData(
id = MembershipConstants.CO_CREATOR_ID,
androidProductId = null,
features = features,
periodValue = 3,
periodType = MembershipPeriodType.PERIOD_TYPE_YEARS,
priceStripeUsdCents = 29900
)
)
}
private fun setupMembershipStatus(tiers: List<MembershipTierData>): MembershipStatus {
return MembershipStatus(
activeTier = TierId(MembershipConstants.EXPLORER_ID),
status = Membership.Status.STATUS_ACTIVE,
dateEnds = 0,
paymentMethod = MembershipPaymentMethod.METHOD_NONE,
anyName = "",
tiers = tiers,
formattedDateEnds = ""
)
}
/**
* Tier - not active and non free | with androidId (Builder) | billing library unavailable
* TierPreview = [Title|Subtitle|ConditionInfo.Price]
* Tier = [Title|Subtitle|Features|ConditionInfo.Price|ButtonInfo]
*/
@Test
fun `test billing unavailable`() = runTest {
turbineScope {
val (features, tiers) = commonTestSetup()
stubMembershipProvider(setupMembershipStatus(tiers))
stubPurchaseState(purchaseState = BillingPurchaseState.Loading)
stubBilling(billingClientState = BillingClientState.NotAvailable)
val viewModel = buildViewModel()
val viewStateFlow = viewModel.viewState.testIn(backgroundScope)
val tierStateFlow = viewModel.tierState.testIn(backgroundScope)
assertIs<MembershipMainState.Loading>(viewStateFlow.awaitItem())
assertIs<MembershipTierState.Hidden>(tierStateFlow.awaitItem())
val conditionInfo = TierConditionInfo.Visible.Price(
price = "$99.01",
period = TierPeriod.Year(1)
)
viewStateFlow.awaitItem().let { result ->
assertIs<MembershipMainState.Default>(result)
val tier: TierPreview = result.tiersPreview.find { it.id.value == MembershipConstants.BUILDER_ID }!!
TestCase.assertEquals(MembershipConstants.BUILDER_ID, tier.id.value)
TestCase.assertEquals(false, tier.isActive)
TestCase.assertEquals(conditionInfo, tier.conditionInfo)
}
viewModel.onTierClicked(TierId(MembershipConstants.BUILDER_ID))
//STATE : BUILDER, NOT ACTIVE, BILLING UNAVAILABLE
tierStateFlow.awaitItem().let { result ->
assertIs<MembershipTierState.Visible>(result)
validateTierView(
tier = result.tier,
expectedFeatures = features,
expectedConditionInfo = conditionInfo,
expectedAnyName = TierAnyName.Hidden,
expectedButtonState = TierButton.Info.Enabled("https://anytype.io/pricing"),
expectedId = MembershipConstants.BUILDER_ID,
expectedActive = false,
expectedEmailState = TierEmail.Hidden
)
}
}
}
/**
* Tier - not active and non free | with androidId (Builder) | billing library unavailable | purchase is exist
* TierPreview = [Title|Subtitle|ConditionInfo.Price]
* Tier = [Title|Subtitle|Features|ConditionInfo.Price|ButtonInfo]
*/
@Test
fun `test billing unavailable, but purchase is exist`() = runTest {
turbineScope {
val (features, tiers) = commonTestSetup()
val purchase = MembershipPurchase(accountId, listOf(androidProductId),
MembershipPurchase.PurchaseState.PURCHASED)
stubPurchaseState(BillingPurchaseState.HasPurchases(listOf(purchase), false))
stubMembershipProvider(setupMembershipStatus(tiers))
stubBilling(billingClientState = BillingClientState.NotAvailable)
val viewModel = buildViewModel()
val viewStateFlow = viewModel.viewState.testIn(backgroundScope)
val tierStateFlow = viewModel.tierState.testIn(backgroundScope)
assertIs<MembershipMainState.Loading>(viewStateFlow.awaitItem())
assertIs<MembershipTierState.Hidden>(tierStateFlow.awaitItem())
val conditionInfo = TierConditionInfo.Visible.Price(
price = "$99.01",
period = TierPeriod.Year(1)
)
viewStateFlow.awaitItem().let { result ->
assertIs<MembershipMainState.Default>(result)
val tier: TierPreview = result.tiersPreview.find { it.id.value == MembershipConstants.BUILDER_ID }!!
TestCase.assertEquals(MembershipConstants.BUILDER_ID, tier.id.value)
TestCase.assertEquals(false, tier.isActive)
TestCase.assertEquals(conditionInfo, tier.conditionInfo)
}
viewModel.onTierClicked(TierId(MembershipConstants.BUILDER_ID))
//STATE : BUILDER, NOT ACTIVE, BILLING UNAVAILABLE
tierStateFlow.awaitItem().let { result ->
assertIs<MembershipTierState.Visible>(result)
validateTierView(
tier = result.tier,
expectedFeatures = features,
expectedConditionInfo = conditionInfo,
expectedAnyName = TierAnyName.Hidden,
expectedButtonState = TierButton.Info.Enabled("https://anytype.io/pricing"),
expectedId = MembershipConstants.BUILDER_ID,
expectedActive = false,
expectedEmailState = TierEmail.Hidden
)
}
}
}
}