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:
parent
64f4d9b8c6
commit
a5dec4e5e1
3 changed files with 230 additions and 25 deletions
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue