Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.revenuecat.purchases.paywalls.components

import androidx.compose.runtime.Immutable
import com.revenuecat.purchases.InternalRevenueCatAPI
import com.revenuecat.purchases.paywalls.components.common.ComponentOverride
import com.revenuecat.purchases.paywalls.components.common.PromoOfferConfig
import com.revenuecat.purchases.paywalls.components.common.ResilientPromoOfferConfigSerializer
import dev.drewhamilton.poko.Poko
Expand All @@ -26,4 +27,17 @@ public class PackageComponent(
@Serializable(with = ResilientPromoOfferConfigSerializer::class)
@SerialName("play_store_offer")
public val playStoreOffer: PromoOfferConfig? = null,
@get:JvmSynthetic
public val visible: Boolean? = null,
@get:JvmSynthetic
public val overrides: List<ComponentOverride<PartialPackageComponent>> = emptyList(),
) : PaywallComponent

@InternalRevenueCatAPI
@Poko
@Serializable
@Immutable
public class PartialPackageComponent(
@get:JvmSynthetic
public val visible: Boolean? = null,
) : PartialComponent
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.revenuecat.purchases.ColorAlias
import com.revenuecat.purchases.JsonTools
import com.revenuecat.purchases.LogHandler
import com.revenuecat.purchases.common.currentLogHandler
import com.revenuecat.purchases.paywalls.components.common.ComponentOverride
import com.revenuecat.purchases.paywalls.components.common.LocalizationKey
import com.revenuecat.purchases.paywalls.components.common.PromoOfferConfig
import com.revenuecat.purchases.paywalls.components.properties.ColorInfo
Expand Down Expand Up @@ -187,6 +188,87 @@ internal class PackageComponentTests(@Suppress("UNUSED_PARAMETER") name: String,
)
),
),
arrayOf(
"visible = true",
Args(
json = """
{
"type": "package",
"package_id": "${"$"}rc_weekly",
"is_selected_by_default": true,
"visible": true,
"stack": {
"type": "stack",
"components": []
}
}
""".trimIndent(),
expected = PackageComponent(
packageId = "${"$"}rc_weekly",
isSelectedByDefault = true,
stack = StackComponent(components = emptyList()),
visible = true,
)
),
),
arrayOf(
"visible = false",
Args(
json = """
{
"type": "package",
"package_id": "${"$"}rc_weekly",
"is_selected_by_default": true,
"visible": false,
"stack": {
"type": "stack",
"components": []
}
}
""".trimIndent(),
expected = PackageComponent(
packageId = "${"$"}rc_weekly",
isSelectedByDefault = true,
stack = StackComponent(components = emptyList()),
visible = false,
)
),
),
arrayOf(
"overrides with visible = false",
Args(
json = """
{
"type": "package",
"package_id": "${"$"}rc_weekly",
"is_selected_by_default": true,
"stack": {
"type": "stack",
"components": []
},
"overrides": [
{
"conditions": [{"type": "intro_offer"}],
"properties": {"visible": false}
}
]
}
""".trimIndent(),
expected = PackageComponent(
packageId = "${"$"}rc_weekly",
isSelectedByDefault = true,
stack = StackComponent(components = emptyList()),
overrides = listOf(
ComponentOverride(
conditions = listOf(
ComponentOverride.Condition.IntroOffer
),
properties = PartialPackageComponent(visible = false),
)
),
)
),
),
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.revenuecat.purchases.ui.revenuecatui.components

import com.revenuecat.purchases.paywalls.components.PartialPackageComponent
import com.revenuecat.purchases.ui.revenuecatui.errors.PaywallValidationError
import com.revenuecat.purchases.ui.revenuecatui.helpers.NonEmptyList
import com.revenuecat.purchases.ui.revenuecatui.helpers.Result
import dev.drewhamilton.poko.Poko

@Poko
internal class PresentedPackagePartial(
@get:JvmSynthetic val partial: PartialPackageComponent,
) : PresentedPartial<PresentedPackagePartial> {

companion object {
@JvmSynthetic
operator fun invoke(
from: PartialPackageComponent,
): Result<PresentedPackagePartial, NonEmptyList<PaywallValidationError>> =
Result.Success(PresentedPackagePartial(partial = from))
}

override fun combine(with: PresentedPackagePartial?): PresentedPackagePartial {
val otherPartial = with?.partial
return PresentedPackagePartial(
partial = PartialPackageComponent(
visible = otherPartial?.visible ?: partial.visible,
),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
@file:JvmSynthetic

package com.revenuecat.purchases.ui.revenuecatui.components.pkg

import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.window.core.layout.WindowWidthSizeClass
import com.revenuecat.purchases.ui.revenuecatui.CustomVariableValue
import com.revenuecat.purchases.ui.revenuecatui.components.ComponentViewState
import com.revenuecat.purchases.ui.revenuecatui.components.ConditionContext
import com.revenuecat.purchases.ui.revenuecatui.components.ScreenCondition
import com.revenuecat.purchases.ui.revenuecatui.components.buildPresentedPartial
import com.revenuecat.purchases.ui.revenuecatui.components.state.PackageAwareDelegate
import com.revenuecat.purchases.ui.revenuecatui.components.style.PackageComponentStyle
import com.revenuecat.purchases.ui.revenuecatui.composables.OfferEligibility
import com.revenuecat.purchases.ui.revenuecatui.data.PaywallState

@Stable
@JvmSynthetic
@Composable
internal fun rememberUpdatedPackageComponentState(
style: PackageComponentStyle,
paywallState: PaywallState.Loaded.Components,
): PackageComponentState = rememberUpdatedPackageComponentState(
style = style,
selectedPackageInfoProvider = { paywallState.selectedPackageInfo },
selectedTabIndexProvider = { paywallState.selectedTabIndex },
selectedOfferEligibilityProvider = { paywallState.selectedOfferEligibility },
customVariablesProvider = { paywallState.mergedCustomVariables },
)

@Suppress("LongParameterList")
@Stable
@JvmSynthetic
@Composable
private fun rememberUpdatedPackageComponentState(
style: PackageComponentStyle,
selectedPackageInfoProvider: () -> PaywallState.Loaded.Components.SelectedPackageInfo?,
selectedTabIndexProvider: () -> Int,
selectedOfferEligibilityProvider: () -> OfferEligibility,
customVariablesProvider: () -> Map<String, CustomVariableValue>,
): PackageComponentState {
val windowSize = currentWindowAdaptiveInfo().windowSizeClass.windowWidthSizeClass

return remember(style) {
PackageComponentState(
initialWindowSize = windowSize,
style = style,
selectedPackageInfoProvider = selectedPackageInfoProvider,
selectedTabIndexProvider = selectedTabIndexProvider,
selectedOfferEligibilityProvider = selectedOfferEligibilityProvider,
customVariablesProvider = customVariablesProvider,
)
}.apply {
update(windowSize = windowSize)
}
}

@Suppress("LongParameterList")
@Stable
internal class PackageComponentState(
initialWindowSize: WindowWidthSizeClass,
private val style: PackageComponentStyle,
private val selectedPackageInfoProvider: () -> PaywallState.Loaded.Components.SelectedPackageInfo?,
private val selectedTabIndexProvider: () -> Int,
private val selectedOfferEligibilityProvider: () -> OfferEligibility,
private val customVariablesProvider: () -> Map<String, CustomVariableValue> = { emptyMap() },
) {
private var windowSize by mutableStateOf(initialWindowSize)

private val packageAwareDelegate = PackageAwareDelegate(
style = style,
selectedPackageInfoProvider = selectedPackageInfoProvider,
selectedTabIndexProvider = selectedTabIndexProvider,
selectedOfferEligibilityProvider = selectedOfferEligibilityProvider,
)

private val presentedPartial by derivedStateOf {
val windowCondition = ScreenCondition.from(windowSize)
val componentState =
if (packageAwareDelegate.isSelected) ComponentViewState.SELECTED else ComponentViewState.DEFAULT

style.overrides.buildPresentedPartial(
windowCondition,
packageAwareDelegate.offerEligibility,
componentState,
conditionContext = ConditionContext(
selectedPackageId = selectedPackageInfoProvider()?.rcPackage?.identifier,
customVariables = customVariablesProvider(),
),
)
}

@get:JvmSynthetic
val visible by derivedStateOf { presentedPartial?.partial?.visible ?: style.visible }

@JvmSynthetic
fun update(windowSize: WindowWidthSizeClass) {
this.windowSize = windowSize
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ internal fun PackageComponentView(
clickHandler: suspend (PaywallAction) -> Unit,
modifier: Modifier = Modifier,
) {
val packageState = rememberUpdatedPackageComponentState(style = style, paywallState = state)

if (!packageState.visible) return

StackComponentView(
style = style.stackComponentStyle,
state = state,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,41 @@ package com.revenuecat.purchases.ui.revenuecatui.components.style
import androidx.compose.runtime.Immutable
import com.revenuecat.purchases.Package
import com.revenuecat.purchases.paywalls.components.properties.Size
import com.revenuecat.purchases.ui.revenuecatui.components.PresentedOverride
import com.revenuecat.purchases.ui.revenuecatui.components.PresentedPackagePartial
import com.revenuecat.purchases.ui.revenuecatui.components.state.PackageContext
import com.revenuecat.purchases.ui.revenuecatui.composables.OfferEligibility
import com.revenuecat.purchases.ui.revenuecatui.helpers.ResolvedOffer

@Suppress("LongParameterList")
@Immutable
internal data class PackageComponentStyle(
@get:JvmSynthetic
val rcPackage: Package,
override val rcPackage: Package,
@get:JvmSynthetic
val isSelectedByDefault: Boolean,
@get:JvmSynthetic
val stackComponentStyle: StackComponentStyle,
@get:JvmSynthetic
val isSelectable: Boolean,
@get:JvmSynthetic
override val resolvedOffer: ResolvedOffer? = null,
@get:JvmSynthetic
override val visible: Boolean,
@get:JvmSynthetic
val overrides: List<PresentedOverride<PresentedPackagePartial>>,
/**
* The resolved Play Store offer for this package, if configured.
* Used for purchase flow and template variables.
* Pre-computed offer eligibility for this package, used for evaluating intro/promo offer conditions
* in package-level overrides.
*/
@get:JvmSynthetic
val resolvedOffer: ResolvedOffer? = null,
) : ComponentStyle {
override val visible: Boolean = stackComponentStyle.visible
override val offerEligibility: OfferEligibility? = null,
) : ComponentStyle, PackageContext {
override val size: Size = stackComponentStyle.size

@get:JvmSynthetic
override val tabIndex: Int? = null

/**
* Unique identifier for this package component, combining package ID and offer ID.
* This allows distinguishing between multiple components that reference the same package
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import com.revenuecat.purchases.ui.revenuecatui.components.LocalizedTextPartial
import com.revenuecat.purchases.ui.revenuecatui.components.PresentedCarouselPartial
import com.revenuecat.purchases.ui.revenuecatui.components.PresentedIconPartial
import com.revenuecat.purchases.ui.revenuecatui.components.PresentedImagePartial
import com.revenuecat.purchases.ui.revenuecatui.components.PresentedPackagePartial
import com.revenuecat.purchases.ui.revenuecatui.components.PresentedStackPartial
import com.revenuecat.purchases.ui.revenuecatui.components.PresentedTabsPartial
import com.revenuecat.purchases.ui.revenuecatui.components.PresentedTimelineItemPartial
Expand Down Expand Up @@ -573,19 +574,33 @@ internal class StyleFactory(
// visually become "selected" if its tab control parent is.
tabControlIndex = null,
) {
val packageOfferEligibility = offerEligibility

val presentedOverridesResult = component.overrides
.toPresentedOverrides(stripRules) { partial ->
PresentedPackagePartial(from = partial)
}
.mapError { nonEmptyListOf(it) }

val (stackComponentStyleResult, purchaseButtons) = withCount(
predicate = { it is PurchaseButtonComponent },
) {
createStackComponentStyle(component.stack)
}

stackComponentStyleResult.map { stack ->
zipOrAccumulate(
first = presentedOverridesResult,
second = stackComponentStyleResult,
) { presentedOverrides, stack ->
PackageComponentStyle(
stackComponentStyle = stack,
rcPackage = rcPackage,
isSelectedByDefault = component.isSelectedByDefault,
isSelectable = purchaseButtons == 0,
resolvedOffer = resolvedOffer,
visible = component.visible ?: DEFAULT_VISIBILITY,
overrides = presentedOverrides,
offerEligibility = packageOfferEligibility,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,7 @@ internal fun PaywallComponent.containsUnsupportedCondition(): Boolean = when (th
-> false
}
}
is PackageComponent -> stack.containsUnsupportedCondition()
is PackageComponent -> overrides.hasUnsupportedCondition() || stack.containsUnsupportedCondition()
is PurchaseButtonComponent -> stack.containsUnsupportedCondition()
is StickyFooterComponent -> stack.containsUnsupportedCondition()
is CarouselComponent -> overrides.hasUnsupportedCondition() ||
Expand Down
Loading