Skip to content

Commit

Permalink
Paywalls: improve handling of lifetime/custom packages (#1363)
Browse files Browse the repository at this point in the history
This uses the existing
`TemplateConfiguration.PackageInfo.currentlySubscribed` to prevent users
from subscribing to a lifetime subscription they already own.

Better consumable support will come in the future.
  • Loading branch information
NachoSoto authored Oct 19, 2023
1 parent 355f6d4 commit 6ec7973
Show file tree
Hide file tree
Showing 10 changed files with 96 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,15 @@ internal fun LoadingPaywall(mode: PaywallMode) {
)

val state = offering.toPaywallState(
VariableDataProvider(
variableDataProvider = VariableDataProvider(
applicationContext,
isInPreviewMode(),
),
setOf(),
mode,
paywallData,
LoadingPaywallConstants.template,
activelySubscribedProductIdentifiers = setOf(),
nonSubscriptionProductIdentifiers = setOf(),
mode = mode,
validatedPaywallData = paywallData,
template = LoadingPaywallConstants.template,
)

when (state) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,11 +214,14 @@ internal class PaywallViewModelImpl(
Logger.w(PaywallValidationErrorStrings.DISPLAYING_DEFAULT)
}
return offering.toPaywallState(
variableDataProvider,
customerInfo.activeSubscriptions,
mode,
displayablePaywall,
template,
variableDataProvider = variableDataProvider,
activelySubscribedProductIdentifiers = customerInfo.activeSubscriptions,
nonSubscriptionProductIdentifiers = customerInfo.nonSubscriptionTransactions
.map { it.productIdentifier }
.toSet(),
mode = mode,
validatedPaywallData = displayablePaywall,
template = template,
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.revenuecat.purchases.ui.revenuecatui.data.processed

import com.revenuecat.purchases.Package
import com.revenuecat.purchases.PackageType
import com.revenuecat.purchases.paywalls.PaywallData
import com.revenuecat.purchases.ui.revenuecatui.errors.PackageConfigurationError
import com.revenuecat.purchases.ui.revenuecatui.helpers.Logger
Expand All @@ -12,6 +13,7 @@ internal object PackageConfigurationFactory {
variableDataProvider: VariableDataProvider,
availablePackages: List<Package>,
activelySubscribedProductIdentifiers: Set<String>,
nonSubscriptionProductIdentifiers: Set<String>,
packageIdsInConfig: List<String>,
default: String?,
localization: PaywallData.LocalizedConfiguration,
Expand All @@ -32,10 +34,25 @@ internal object PackageConfigurationFactory {
return Result.failure(PackageConfigurationError("No packages found for ids $packageIdsInConfig"))
}
val packageInfos = filteredRCPackages.map {
val currentlySubscribed = when (it.packageType) {
PackageType.ANNUAL,
PackageType.SIX_MONTH,
PackageType.THREE_MONTH,
PackageType.TWO_MONTH,
PackageType.MONTHLY,
PackageType.WEEKLY,
-> activelySubscribedProductIdentifiers.contains(it.product.id)

PackageType.LIFETIME, PackageType.CUSTOM,
-> nonSubscriptionProductIdentifiers.contains(it.product.id)

PackageType.UNKNOWN -> false
}

TemplateConfiguration.PackageInfo(
rcPackage = it,
localization = ProcessedLocalizedConfiguration.create(variableDataProvider, localization, it, locale),
currentlySubscribed = activelySubscribedProductIdentifiers.contains(it.product.id),
currentlySubscribed = currentlySubscribed,
discountRelativeToMostExpensivePerMonth = null, // TODO-PAYWALLS: Support discount UI
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ internal object TemplateConfigurationFactory {
paywallData: PaywallData,
availablePackages: List<Package>,
activelySubscribedProductIdentifiers: Set<String>,
nonSubscriptionProductIdentifiers: Set<String>,
template: PaywallTemplate,
): Result<TemplateConfiguration> {
val (locale, localizedConfiguration) = paywallData.localizedConfiguration
Expand All @@ -27,6 +28,7 @@ internal object TemplateConfigurationFactory {
variableDataProvider = variableDataProvider,
availablePackages = availablePackages,
activelySubscribedProductIdentifiers = activelySubscribedProductIdentifiers,
nonSubscriptionProductIdentifiers = nonSubscriptionProductIdentifiers,
packageIdsInConfig = paywallData.config.packageIds,
default = paywallData.config.defaultPackage,
localization = localizedConfiguration,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ internal object TestData {
Packages.weekly,
Packages.monthly,
Packages.annual,
Packages.lifetime,
),
metadata = mapOf(),
paywall = template2,
Expand Down Expand Up @@ -272,11 +273,12 @@ internal class MockViewModel(

private val _state = MutableStateFlow(
offering.toPaywallState(
VariableDataProvider(MockApplicationContext()),
setOf(),
mode,
offering.paywall!!,
PaywallTemplate.fromId(offering.paywall!!.templateName)!!,
variableDataProvider = VariableDataProvider(MockApplicationContext()),
activelySubscribedProductIdentifiers = setOf(),
nonSubscriptionProductIdentifiers = setOf(),
mode = mode,
validatedPaywallData = offering.paywall!!,
template = PaywallTemplate.fromId(offering.paywall!!.templateName)!!,
),
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ internal val TestData.template2: PaywallData
packageIds = listOf(
PackageType.ANNUAL.identifier!!,
PackageType.MONTHLY.identifier!!,
PackageType.LIFETIME.identifier!!,
),
defaultPackage = PackageType.MONTHLY.identifier!!,
images = TestData.Constants.images,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,11 @@ private fun PaywallData.validate(): Result<PaywallTemplate> {
return Result.success(template)
}

@Suppress("ReturnCount", "TooGenericExceptionCaught")
@Suppress("ReturnCount", "TooGenericExceptionCaught", "LongParameterList")
internal fun Offering.toPaywallState(
variableDataProvider: VariableDataProvider,
activelySubscribedProductIdentifiers: Set<String>,
nonSubscriptionProductIdentifiers: Set<String>,
mode: PaywallMode,
validatedPaywallData: PaywallData,
template: PaywallTemplate,
Expand All @@ -81,6 +82,7 @@ internal fun Offering.toPaywallState(
paywallData = validatedPaywallData,
availablePackages = availablePackages,
activelySubscribedProductIdentifiers = activelySubscribedProductIdentifiers,
nonSubscriptionProductIdentifiers = nonSubscriptionProductIdentifiers,
template,
)
val templateConfiguration = createTemplateConfigurationResult.getOrElse {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.revenuecat.purchases.Offerings
import com.revenuecat.purchases.Package
import com.revenuecat.purchases.PurchaseResult
import com.revenuecat.purchases.models.StoreTransaction
import com.revenuecat.purchases.models.Transaction
import com.revenuecat.purchases.paywalls.PaywallData
import com.revenuecat.purchases.ui.revenuecatui.PaywallListener
import com.revenuecat.purchases.ui.revenuecatui.PaywallMode
Expand All @@ -35,6 +36,8 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.util.Date
import java.util.UUID

@RunWith(AndroidJUnit4::class)
class PaywallViewModelTest {
Expand Down Expand Up @@ -111,7 +114,10 @@ class PaywallViewModelTest {

@Test
fun `Should load default offering`() {
val model = create(activeSubscriptions = arrayOf(TestData.Packages.monthly.product.id))
val model = create(
activeSubscriptions = setOf(TestData.Packages.monthly.product.id),
nonSubscriptionTransactionProductIdentifiers = setOf(TestData.Packages.lifetime.product.id),
)

coVerify { purchases.awaitOfferings() }

Expand All @@ -128,6 +134,8 @@ class PaywallViewModelTest {
.isTrue
assertThat(state.templateConfiguration.packages.packageIsCurrentlySubscribed(TestData.Packages.annual))
.isFalse
assertThat(state.templateConfiguration.packages.packageIsCurrentlySubscribed(TestData.Packages.lifetime))
.isTrue
}

@Test
Expand Down Expand Up @@ -226,9 +234,11 @@ class PaywallViewModelTest {

private fun create(
offering: Offering? = null,
vararg activeSubscriptions: String,
activeSubscriptions: Set<String> = setOf(),
nonSubscriptionTransactionProductIdentifiers: Set<String> = setOf(),
): PaywallViewModelImpl {
mockActiveSubscriptions(*activeSubscriptions)
mockActiveSubscriptions(activeSubscriptions)
mockNonSubscriptionTransactions(nonSubscriptionTransactionProductIdentifiers)

return PaywallViewModelImpl(
MockApplicationContext(),
Expand All @@ -241,8 +251,21 @@ class PaywallViewModelTest {
)
}

private fun mockActiveSubscriptions(vararg subscriptions: String) {
every { customerInfo.activeSubscriptions } returns setOf(*subscriptions)
private fun mockActiveSubscriptions(subscriptions: Set<String>) {
every { customerInfo.activeSubscriptions } returns subscriptions
}

private fun mockNonSubscriptionTransactions(productIdentifiers: Set<String>) {
every { customerInfo.nonSubscriptionTransactions } returns productIdentifiers
.map {
Transaction(
UUID.randomUUID().toString(),
UUID.randomUUID().toString(),
it,
it,
Date()
)
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,22 @@ internal class TemplateConfigurationFactoryTest {
fun setUp() {
variableDataProvider = VariableDataProvider(MockApplicationContext())
val result = TemplateConfigurationFactory.create(
variableDataProvider,
paywallMode,
TestData.template2,
listOf(TestData.Packages.weekly, TestData.Packages.monthly, TestData.Packages.annual),
setOf(
TestData.Packages.monthly.product.id
variableDataProvider = variableDataProvider,
mode = paywallMode,
paywallData = TestData.template2,
availablePackages = listOf(
TestData.Packages.weekly,
TestData.Packages.monthly,
TestData.Packages.annual,
TestData.Packages.lifetime,
),
PaywallTemplate.TEMPLATE_2,
activelySubscribedProductIdentifiers = setOf(
TestData.Packages.monthly.product.id,
),
nonSubscriptionProductIdentifiers = setOf(
TestData.Packages.lifetime.product.id
),
template = PaywallTemplate.TEMPLATE_2,
)
template2Configuration = result.getOrNull()!!
}
Expand Down Expand Up @@ -63,13 +71,15 @@ internal class TemplateConfigurationFactoryTest {

val annualPackage = getPackageInfo(TestData.Packages.annual, currentlySubscribed = false)
val monthlyPackage = getPackageInfo(TestData.Packages.monthly, currentlySubscribed = true)
val lifetime = getPackageInfo(TestData.Packages.lifetime, currentlySubscribed = true)

val expectedConfiguration = TemplateConfiguration.PackageConfiguration.Multiple(
first = annualPackage,
default = monthlyPackage,
all = listOf(
annualPackage,
monthlyPackage,
lifetime
),
)

Expand Down Expand Up @@ -97,24 +107,28 @@ internal class TemplateConfigurationFactoryTest {
PackageType.ANNUAL -> "Annual"
PackageType.MONTHLY -> "Monthly"
PackageType.WEEKLY -> "Weekly"
PackageType.LIFETIME -> "Lifetime"
else -> error("Unknown package type ${rcPackage.packageType}")
}
val callToAction = when(rcPackage.packageType) {
PackageType.ANNUAL -> "Subscribe for $67.99/yr"
PackageType.MONTHLY -> "Subscribe for $7.99/mth"
PackageType.WEEKLY -> "Subscribe for $1.99/wk"
PackageType.LIFETIME -> "Subscribe for $1,000"
else -> error("Unknown package type ${rcPackage.packageType}")
}
val offerDetails = when(rcPackage.packageType) {
PackageType.ANNUAL -> "$67.99/yr ($5.67/mth)"
PackageType.MONTHLY -> "$7.99/mth"
PackageType.WEEKLY -> "$1.99/wk ($7.96/mth)"
PackageType.LIFETIME -> "$1,000"
else -> error("Unknown package type ${rcPackage.packageType}")
}
val offerDetailsWithIntroOffer = when(rcPackage.packageType) {
PackageType.ANNUAL -> "$67.99/yr ($5.67/mth) after 1 month trial"
PackageType.MONTHLY -> "$7.99/mth after trial"
PackageType.WEEKLY -> "$1.99/wk ($7.96/mth) after trial"
PackageType.LIFETIME -> "$1,000 after trial"
else -> error("Unknown package type ${rcPackage.packageType}")
}
val processedLocalization = ProcessedLocalizedConfiguration(
Expand Down
3 changes: 2 additions & 1 deletion ui/revenuecatui/src/test/resources/default_template.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
},
"packages" : [
"$rc_annual",
"$rc_monthly"
"$rc_monthly",
"$rc_lifetime"
],
"privacy_url" : null,
"tos_url" : null
Expand Down

0 comments on commit 6ec7973

Please sign in to comment.