Repository: RevenueCat/purchases-android
Pull request #2408: [Paywalls V2] Support missing packages in paywalls
Author: login: tonidero
Labels: name: pr:fix
description: A bug fix
name: pr:RevenueCatUI
name: feat:PaywallV2
Created: 2025-05-29T16:47:57Z | Merged: 2025-05-30T08:13:22Z

### Description
This makes it so we render the paywall even if there is a missing product from the play store. It will only fail and fallback to the fallback paywall in case no packages are found.


Diff:
diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/LoadedPaywallComponents.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/LoadedPaywallComponents.kt
index 5f27821fbf..07cbba5a31 100644
--- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/LoadedPaywallComponents.kt
+++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/LoadedPaywallComponents.kt
@@ -46,6 +46,7 @@ import com.revenuecat.purchases.paywalls.components.properties.TwoDimensionalAli
 import com.revenuecat.purchases.ui.revenuecatui.components.modifier.background
 import com.revenuecat.purchases.ui.revenuecatui.components.properties.rememberBackgroundStyle
 import com.revenuecat.purchases.ui.revenuecatui.data.PaywallState
+import com.revenuecat.purchases.ui.revenuecatui.data.testdata.TestData
 import com.revenuecat.purchases.ui.revenuecatui.helpers.getOrThrow
 import com.revenuecat.purchases.ui.revenuecatui.helpers.toComponentsPaywallState
 import java.net.URL
@@ -102,6 +103,7 @@ private fun LoadedPaywallComponents_Preview() {
                             text = LocalizationKey("hello-world"),
                             color = ColorScheme(light = ColorInfo.Hex(Color.Black.toArgb())),
                         ),
+                        TestData.Components.monthlyPackageComponent,
                     ),
                     dimension = Vertical(alignment = CENTER, distribution = START),
                     backgroundColor = ColorScheme(light = ColorInfo.Hex(Color.Cyan.toArgb())),
@@ -155,7 +157,7 @@ private fun LoadedPaywallComponents_Preview() {
         identifier = "id",
         serverDescription = "description",
         metadata = emptyMap(),
-        availablePackages = emptyList(),
+        availablePackages = listOf(TestData.Packages.monthly),
         paywallComponents = Offering.PaywallComponents(previewUiConfig(), data),
     )
     val validated = offering.validatePaywallComponentsDataOrNullForPreviews()?.getOrThrow()!!
@@ -194,7 +196,7 @@ private fun LoadedPaywallComponents_Preview_Bless() {
                 stack = StackComponent(
                     components = listOf(
                         StackComponent(
-                            components = emptyList(),
+                            components = listOf(TestData.Components.monthlyPackageComponent),
                             dimension = ZLayer(alignment = TwoDimensionalAlignment.CENTER),
                             size = Size(width = Fill, height = Fill),
                             backgroundColor = ColorScheme(
@@ -335,7 +337,7 @@ private fun LoadedPaywallComponents_Preview_Bless() {
         identifier = "id",
         serverDescription = "description",
         metadata = emptyMap(),
-        availablePackages = emptyList(),
+        availablePackages = listOf(TestData.Packages.monthly),
         paywallComponents = Offering.PaywallComponents(previewUiConfig(), data),
     )
     val validated = offering.validatePaywallComponentsDataOrNullForPreviews()?.getOrThrow()!!
diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/PreviewHelpers.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/PreviewHelpers.kt
index 6a1177674f..f052efaa18 100644
--- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/PreviewHelpers.kt
+++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/PreviewHelpers.kt
@@ -61,6 +61,7 @@ import com.revenuecat.purchases.ui.revenuecatui.components.style.IconComponentSt
 import com.revenuecat.purchases.ui.revenuecatui.components.style.StackComponentStyle
 import com.revenuecat.purchases.ui.revenuecatui.components.style.TextComponentStyle
 import com.revenuecat.purchases.ui.revenuecatui.data.PaywallState
+import com.revenuecat.purchases.ui.revenuecatui.data.testdata.TestData
 import com.revenuecat.purchases.ui.revenuecatui.errors.PaywallValidationError
 import com.revenuecat.purchases.ui.revenuecatui.helpers.NonEmptyList
 import com.revenuecat.purchases.ui.revenuecatui.helpers.NonEmptyMap
@@ -86,7 +87,7 @@ internal fun previewEmptyState(): PaywallState.Loaded.Components {
         componentsConfig = ComponentsConfig(
             base = PaywallComponentsConfig(
                 // This would normally contain at least one TextComponent, but that's not needed for previews.
-                stack = StackComponent(components = emptyList()),
+                stack = StackComponent(components = listOf(TestData.Components.monthlyPackageComponent)),
                 background = Background.Color(ColorScheme(light = ColorInfo.Hex(Color.White.toArgb()))),
                 stickyFooter = null,
             ),
@@ -102,7 +103,7 @@ internal fun previewEmptyState(): PaywallState.Loaded.Components {
         identifier = "identifier",
         serverDescription = "serverDescription",
         metadata = emptyMap(),
-        availablePackages = emptyList(),
+        availablePackages = listOf(TestData.Packages.monthly),
         paywallComponents = Offering.PaywallComponents(
             uiConfig = UiConfig(
                 localizations = nonEmptyMapOf(LocaleId("en_US") to variableLocalizationKeysForEnUs()),
diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/style/StyleFactory.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/style/StyleFactory.kt
index 02fc468dbb..c0cc3878fb 100644
--- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/style/StyleFactory.kt
+++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/style/StyleFactory.kt
@@ -59,6 +59,7 @@ import com.revenuecat.purchases.ui.revenuecatui.data.PaywallState.Loaded.Compone
 import com.revenuecat.purchases.ui.revenuecatui.errors.PaywallValidationError
 import com.revenuecat.purchases.ui.revenuecatui.extensions.toOrientation
 import com.revenuecat.purchases.ui.revenuecatui.extensions.toPageControlStyles
+import com.revenuecat.purchases.ui.revenuecatui.helpers.Logger
 import com.revenuecat.purchases.ui.revenuecatui.helpers.NonEmptyList
 import com.revenuecat.purchases.ui.revenuecatui.helpers.NonEmptyMap
 import com.revenuecat.purchases.ui.revenuecatui.helpers.Result
@@ -163,6 +164,7 @@ internal class StyleFactory(
                                 topWindowInsetsApplied = component.components.firstOrNull()?.isHeaderImage == true
                                 topWindowInsetsApplied
                             }
+
                             is Dimension.Horizontal,
                             is Dimension.Vertical,
                             -> false
@@ -324,6 +326,7 @@ internal class StyleFactory(
                     is StickyFooterComponentStyle -> copy(
                         stackComponentStyle = stackComponentStyle.copy(applyBottomWindowInsets = true),
                     )
+
                     else -> this
                 } as T
             } else {
@@ -423,17 +426,18 @@ internal class StyleFactory(
 
     private fun StyleFactoryScope.createPackageComponentStyle(
         component: PackageComponent,
-    ): Result<PackageComponentStyle, NonEmptyList<PaywallValidationError>> =
-        offering.getPackageOrNull(component.packageId)
-            .errorIfNull(
-                nonEmptyListOf(
-                    PaywallValidationError.MissingPackage(
+    ): Result<PackageComponentStyle?, NonEmptyList<PaywallValidationError>> =
+        Result.Success(offering.getPackageOrNull(component.packageId))
+            .flatMap { rcPackage ->
+                if (rcPackage == null) {
+                    val error = PaywallValidationError.MissingPackage(
                         offeringId = offering.identifier,
                         missingPackageId = component.packageId,
                         allPackageIds = offering.availablePackages.map { it.identifier },
-                    ),
-                ),
-            ).flatMap { rcPackage ->
+                    )
+                    Logger.w(error.message)
+                    return Result.Success(null)
+                }
                 withSelectedScope(
                     packageInfo = AvailablePackages.Info(
                         pkg = rcPackage,
diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallState.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallState.kt
index c370f87e1c..0554c9c542 100644
--- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallState.kt
+++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallState.kt
@@ -112,6 +112,9 @@ internal sealed interface PaywallState {
                         packagesOutsideTabs = packagesOutsideTabs + with?.packagesOutsideTabs.orEmpty(),
                         packagesByTab = packagesByTab.ifEmpty { with?.packagesByTab.orEmpty() },
                     )
+
+                val hasAnyPackages: Boolean
+                    get() = packagesOutsideTabs.isNotEmpty() || packagesByTab.isNotEmpty()
             }
 
             data class SelectedPackageInfo(
diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/testdata/TestData.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/testdata/TestData.kt
index 2fa4ed0752..4429ef8da8 100644
--- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/testdata/TestData.kt
+++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/testdata/TestData.kt
@@ -15,6 +15,8 @@ import com.revenuecat.purchases.models.Period
 import com.revenuecat.purchases.models.Price
 import com.revenuecat.purchases.models.TestStoreProduct
 import com.revenuecat.purchases.paywalls.PaywallData
+import com.revenuecat.purchases.paywalls.components.PackageComponent
+import com.revenuecat.purchases.paywalls.components.StackComponent
 import com.revenuecat.purchases.ui.revenuecatui.PaywallMode
 import com.revenuecat.purchases.ui.revenuecatui.R
 import com.revenuecat.purchases.ui.revenuecatui.data.PaywallState
@@ -390,6 +392,14 @@ internal object TestData {
             ),
         )
     }
+
+    object Components {
+        val monthlyPackageComponent = PackageComponent(
+            packageId = PackageType.MONTHLY.identifier!!,
+            isSelectedByDefault = false,
+            stack = StackComponent(components = emptyList()),
+        )
+    }
 }
 
 internal class MockResourceProvider(
diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/errors/PaywallValidationError.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/errors/PaywallValidationError.kt
index 437049f843..642d343005 100644
--- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/errors/PaywallValidationError.kt
+++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/errors/PaywallValidationError.kt
@@ -38,6 +38,7 @@ internal sealed class PaywallValidationError : Throwable() {
             is AllLocalizationsMissing -> message
             is AllVariableLocalizationsMissing -> message
             is MissingPackage -> message
+            is MissingAllPackages -> message
             is MissingColorAlias -> message
             is AliasedColorIsAlias -> message
             is MissingFontAlias -> message
@@ -92,6 +93,14 @@ internal sealed class PaywallValidationError : Throwable() {
             PaywallValidationErrorStrings.MISSING_PACKAGE
                 .format(missingPackageId, offeringId, allPackageIds.joinToString())
     }
+    data class MissingAllPackages(
+        val offeringId: String,
+        val allPackageIds: Collection<String>,
+    ) : PaywallValidationError() {
+        override val message: String =
+            PaywallValidationErrorStrings.MISSING_ALL_PACKAGES
+                .format(offeringId, allPackageIds.joinToString())
+    }
     data class MissingColorAlias(
         val alias: ColorAlias,
     ) : PaywallValidationError() {
diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/helpers/OfferingToStateMapper.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/helpers/OfferingToStateMapper.kt
index 0258fd62e9..b93a84c922 100644
--- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/helpers/OfferingToStateMapper.kt
+++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/helpers/OfferingToStateMapper.kt
@@ -166,6 +166,20 @@ internal fun Offering.validatePaywallComponentsDataOrNull(
             .orSuccessfullyNull(),
         third = config.background.toBackgroundStyles(aliases = colorAliases),
     ) { backendRootComponentResult, stickyFooterResult, background ->
+        val hasAnyPackages = backendRootComponentResult.availablePackages.hasAnyPackages ||
+            stickyFooterResult?.availablePackages?.hasAnyPackages ?: false
+        // Check if there are any packages available in the offering
+        if (!hasAnyPackages) {
+            return RcResult.Error(
+                nonEmptyListOf(
+                    PaywallValidationError.MissingAllPackages(
+                        identifier,
+                        availablePackages.map { it.identifier },
+                    ),
+                ),
+            )
+        }
+
         val backendRootComponent = backendRootComponentResult.componentStyle
         val stickyFooter = stickyFooterResult?.componentStyle
         // This is a temporary hack to make the root component fill the screen. This will be removed once we have a
diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/strings/PaywallValidationErrorStrings.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/strings/PaywallValidationErrorStrings.kt
index ba3a5101d8..6e44422887 100644
--- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/strings/PaywallValidationErrorStrings.kt
+++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/strings/PaywallValidationErrorStrings.kt
@@ -24,6 +24,10 @@ internal object PaywallValidationErrorStrings {
         "The Paywall references a package with id '%s', but Offering '%s' does not contain such a package. " +
             "It has these packages instead: [%s]. Either add the missing package to the Offering or remove it from " +
             "the Paywall."
+    const val MISSING_ALL_PACKAGES =
+        "Could not find any packages referenced by paywall in Offering '%s'. These packages were found: " +
+            "[%s]. Either add the correct packages to the Offering or remove them from " +
+            "the Paywall."
     const val MISSING_COLOR_ALIAS = "Aliased color '%s' does not exist."
     const val ALIASED_COLOR_IS_ALIAS = "Aliased color '%s' has an aliased value '%s', which is not allowed."
     const val MISSING_FONT_ALIAS = "Aliased font '%s' does not exist."
diff --git a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/PaywallComponentDataValidationTests.kt b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/PaywallComponentDataValidationTests.kt
index e7cf386400..d17f052e8f 100644
--- a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/PaywallComponentDataValidationTests.kt
+++ b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/PaywallComponentDataValidationTests.kt
@@ -75,7 +75,8 @@ class PaywallComponentDataValidationTests {
                             TextComponent(
                                 text = localizationKey,
                                 color = ColorScheme(light = ColorInfo.Hex(Color.Black.toArgb()))
-                            )
+                            ),
+                            TestData.Components.monthlyPackageComponent,
                         ),
                         size = Size(width = SizeConstraint.Fill, height = SizeConstraint.Fit),
                     ),
@@ -93,7 +94,7 @@ class PaywallComponentDataValidationTests {
             identifier = "identifier",
             serverDescription = "serverDescription",
             metadata = emptyMap(),
-            availablePackages = emptyList(),
+            availablePackages = listOf(TestData.Packages.monthly),
             paywallComponents = Offering.PaywallComponents(UiConfig(), paywallComponents),
         )
     }
@@ -114,7 +115,7 @@ class PaywallComponentDataValidationTests {
             assetBaseURL = URL("https://assets.pawwalls.com"),
             componentsConfig = ComponentsConfig(
                 base = PaywallComponentsConfig(
-                    stack = StackComponent(components = emptyList()),
+                    stack = StackComponent(components = listOf(TestData.Components.monthlyPackageComponent)),
                     background = Background.Color(ColorScheme(light = ColorInfo.Hex(Color.White.toArgb()))),
                     stickyFooter = null,
                 ),
@@ -126,7 +127,7 @@ class PaywallComponentDataValidationTests {
             identifier = "identifier",
             serverDescription = "serverDescription",
             metadata = emptyMap(),
-            availablePackages = emptyList(),
+            availablePackages = listOf(TestData.Packages.monthly),
             paywallComponents = Offering.PaywallComponents(UiConfig(), data),
         )
 
@@ -160,7 +161,7 @@ class PaywallComponentDataValidationTests {
             identifier = "identifier",
             serverDescription = "serverDescription",
             metadata = emptyMap(),
-            availablePackages = emptyList(),
+            availablePackages = listOf(TestData.Packages.monthly),
             paywallComponents = Offering.PaywallComponents(UiConfig(), data),
         )
 
@@ -217,7 +218,7 @@ class PaywallComponentDataValidationTests {
                 assetBaseURL = URL("https://assets.pawwalls.com"),
                 componentsConfig = ComponentsConfig(
                     base = PaywallComponentsConfig(
-                        stack = StackComponent(components = emptyList()),
+                        stack = StackComponent(components = listOf(TestData.Components.monthlyPackageComponent)),
                         background = Background.Color(ColorScheme(light = ColorInfo.Hex(Color.White.toArgb()))),
                         stickyFooter = null,
                     ),
@@ -229,7 +230,7 @@ class PaywallComponentDataValidationTests {
                 identifier = "identifier",
                 serverDescription = "serverDescription",
                 metadata = emptyMap(),
-                availablePackages = emptyList(),
+                availablePackages = listOf(TestData.Packages.monthly),
                 paywallComponents = Offering.PaywallComponents(UiConfig(localizations = variableLocalizations), data),
             )
 
@@ -322,7 +323,7 @@ class PaywallComponentDataValidationTests {
                 base = PaywallComponentsConfig(
                     stack = StackComponent(
                         // There are no TextComponents using the localizations.
-                        components = emptyList()
+                        components = listOf(TestData.Components.monthlyPackageComponent)
                     ),
                     background = Background.Color(ColorScheme(light = ColorInfo.Hex(Color.White.toArgb()))),
                     stickyFooter = null,
@@ -347,7 +348,7 @@ class PaywallComponentDataValidationTests {
             identifier = "identifier",
             serverDescription = "serverDescription",
             metadata = emptyMap(),
-            availablePackages = emptyList(),
+            availablePackages = listOf(TestData.Packages.monthly),
             paywallComponents = Offering.PaywallComponents(UiConfig(), data),
         )
 
@@ -405,6 +406,7 @@ class PaywallComponentDataValidationTests {
                                 color = textColor,
                                 fontName = secondaryFontAlias,
                             ),
+                            TestData.Components.monthlyPackageComponent,
                         ),
                     ),
                     background = Background.Color(ColorScheme(light = ColorInfo.Hex(Color.White.toArgb()))),
@@ -423,7 +425,7 @@ class PaywallComponentDataValidationTests {
             identifier = "identifier",
             serverDescription = "serverDescription",
             metadata = emptyMap(),
-            availablePackages = emptyList(),
+            availablePackages = listOf(TestData.Packages.monthly),
             paywallComponents = Offering.PaywallComponents(uiConfig, data),
         )
 
@@ -434,7 +436,7 @@ class PaywallComponentDataValidationTests {
         val validatedComponents = validated as PaywallValidationResult.Components
         assertNull(validatedComponents.errors)
         val stack = validatedComponents.stack as StackComponentStyle
-        assertEquals(2, stack.children.size)
+        assertEquals(3, stack.children.size)
         val text1 = stack.children[0] as TextComponentStyle
         val text2 = stack.children[1] as TextComponentStyle
         assertEquals(robotoFont, text1.fontSpec)
@@ -485,6 +487,7 @@ class PaywallComponentDataValidationTests {
                                 color = textColor,
                                 fontName = missingFontAlias3,
                             ),
+                            TestData.Components.monthlyPackageComponent,
                         ),
                     ),
                     background = Background.Color(ColorScheme(light = ColorInfo.Hex(Color.White.toArgb()))),
@@ -503,7 +506,7 @@ class PaywallComponentDataValidationTests {
             identifier = "identifier",
             serverDescription = "serverDescription",
             metadata = emptyMap(),
-            availablePackages = emptyList(),
+            availablePackages = listOf(TestData.Packages.monthly),
             paywallComponents = Offering.PaywallComponents(uiConfig, data),
         )
 
@@ -550,6 +553,7 @@ class PaywallComponentDataValidationTests {
                                     )
                                 )
                             ),
+                            TestData.Components.monthlyPackageComponent,
                         ),
                     ),
                     background = Background.Color(ColorScheme(light = ColorInfo.Hex(Color.White.toArgb()))),
@@ -568,7 +572,7 @@ class PaywallComponentDataValidationTests {
             identifier = "identifier",
             serverDescription = "serverDescription",
             metadata = emptyMap(),
-            availablePackages = emptyList(),
+            availablePackages = listOf(TestData.Packages.monthly),
             paywallComponents = Offering.PaywallComponents(uiConfig, data),
         )
 
@@ -621,7 +625,8 @@ class PaywallComponentDataValidationTests {
                                         )
                                     )
                                 )
-                            )
+                            ),
+                            TestData.Components.monthlyPackageComponent,
                         )
                     ),
                     background = Background.Color(ColorScheme(light = ColorInfo.Hex(Color.White.toArgb()))),
@@ -639,7 +644,7 @@ class PaywallComponentDataValidationTests {
             identifier = "identifier",
             serverDescription = "serverDescription",
             metadata = emptyMap(),
-            availablePackages = emptyList(),
+            availablePackages = listOf(TestData.Packages.monthly),
             paywallComponents = Offering.PaywallComponents(UiConfig(), data),
         )
 
@@ -676,7 +681,8 @@ class PaywallComponentDataValidationTests {
                                     StackComponent(components = listOf()),
                                     StackComponent(components = listOf()),
                                 )
-                            )
+                            ),
+                            TestData.Components.monthlyPackageComponent,
                         )
                     ),
                     background = Background.Color(ColorScheme(light = ColorInfo.Hex(Color.White.toArgb()))),
@@ -694,7 +700,7 @@ class PaywallComponentDataValidationTests {
             identifier = "identifier",
             serverDescription = "serverDescription",
             metadata = emptyMap(),
-            availablePackages = emptyList(),
+            availablePackages = listOf(TestData.Packages.monthly),
             paywallComponents = Offering.PaywallComponents(UiConfig(), data),
         )
 
@@ -728,7 +734,8 @@ class PaywallComponentDataValidationTests {
                                     StackComponent(components = listOf()),
                                     StackComponent(components = listOf()),
                                 )
-                            )
+                            ),
+                            TestData.Components.monthlyPackageComponent,
                         )
                     ),
                     background = Background.Color(ColorScheme(light = ColorInfo.Hex(Color.White.toArgb()))),
@@ -760,7 +767,7 @@ class PaywallComponentDataValidationTests {
             identifier = "identifier",
             serverDescription = "serverDescription",
             metadata = emptyMap(),
-            availablePackages = emptyList(),
+            availablePackages = listOf(TestData.Packages.monthly),
             paywallComponents = Offering.PaywallComponents(UiConfig(), data),
         )
 
@@ -768,8 +775,8 @@ class PaywallComponentDataValidationTests {
         val validated = offering.validatedPaywall(TestData.Constants.currentColorScheme, MockResourceProvider())
 
         // Assert
-        assertTrue(validated is PaywallValidationResult.Components)
         assertNull(validated.errors)
+        assertTrue(validated is PaywallValidationResult.Components)
         val validatedComponents = validated as PaywallValidationResult.Components
         val actualStickyFooter = validatedComponents.stickyFooter as StickyFooterComponentStyle?
         assertNotNull(actualStickyFooter)
@@ -796,7 +803,8 @@ class PaywallComponentDataValidationTests {
                                     StackComponent(components = listOf()),
                                     StackComponent(components = listOf()),
                                 )
-                            )
+                            ),
+                            TestData.Components.monthlyPackageComponent,
                         )
                     ),
                     background = Background.Color(ColorScheme(light = ColorInfo.Hex(Color.White.toArgb()))),
@@ -814,7 +822,7 @@ class PaywallComponentDataValidationTests {
             identifier = "identifier",
             serverDescription = "serverDescription",
             metadata = emptyMap(),
-            availablePackages = emptyList(),
+            availablePackages = listOf(TestData.Packages.monthly),
             paywallComponents = Offering.PaywallComponents(UiConfig(), data),
         )
 
@@ -828,4 +836,91 @@ class PaywallComponentDataValidationTests {
         val actualStack = validatedComponents.stack as StackComponentStyle
         assertTrue(actualStack.applyBottomWindowInsets)
     }
+
+    @Test
+    fun `Should fail to validate if no package component is found`() {
+        // Arrange
+        val defaultLocale = LocaleId("en_US")
+        val data = PaywallComponentsData(
+            templateName = "template",
+            assetBaseURL = URL("https://assets.pawwalls.com"),
+            componentsConfig = ComponentsConfig(
+                base = PaywallComponentsConfig(
+                    stack = StackComponent(
+                        dimension = Dimension.Vertical(HorizontalAlignment.CENTER, START),
+                        components = listOf(
+                            TestData.Components.monthlyPackageComponent,
+                        )
+                    ),
+                    background = Background.Color(ColorScheme(light = ColorInfo.Hex(Color.White.toArgb()))),
+                    stickyFooter = null,
+                ),
+            ),
+            componentsLocalizations = mapOf(
+                defaultLocale to mapOf(
+                    LocalizationKey("key1") to LocalizationData.Text("value1"),
+                ),
+            ),
+            defaultLocaleIdentifier = defaultLocale,
+        )
+        val offering = Offering(
+            identifier = "identifier",
+            serverDescription = "serverDescription",
+            metadata = emptyMap(),
+            availablePackages = listOf(TestData.Packages.annual, TestData.Packages.lifetime),
+            paywallComponents = Offering.PaywallComponents(UiConfig(), data),
+        )
+
+        // Act
+        val validated = offering.validatedPaywall(TestData.Constants.currentColorScheme, MockResourceProvider())
+
+        // Assert
+        assertNotNull(validated.errors)
+        assertEquals(validated.errors?.size, 1)
+        assertEquals(validated.errors?.first(), PaywallValidationError.MissingAllPackages(
+            offeringId = "identifier",
+            allPackageIds = listOf(TestData.Packages.annual.identifier, TestData.Packages.lifetime.identifier)
+        ))
+    }
+
+    @Test
+    fun `Should validate if at least one package component is found`() {
+        // Arrange
+        val defaultLocale = LocaleId("en_US")
+        val data = PaywallComponentsData(
+            templateName = "template",
+            assetBaseURL = URL("https://assets.pawwalls.com"),
+            componentsConfig = ComponentsConfig(
+                base = PaywallComponentsConfig(
+                    stack = StackComponent(
+                        dimension = Dimension.Vertical(HorizontalAlignment.CENTER, START),
+                        components = listOf(
+                            TestData.Components.monthlyPackageComponent,
+                        )
+                    ),
+                    background = Background.Color(ColorScheme(light = ColorInfo.Hex(Color.White.toArgb()))),
+                    stickyFooter = null,
+                ),
+            ),
+            componentsLocalizations = mapOf(
+                defaultLocale to mapOf(
+                    LocalizationKey("key1") to LocalizationData.Text("value1"),
+                ),
+            ),
+            defaultLocaleIdentifier = defaultLocale,
+        )
+        val offering = Offering(
+            identifier = "identifier",
+            serverDescription = "serverDescription",
+            metadata = emptyMap(),
+            availablePackages = listOf(TestData.Packages.annual, TestData.Packages.monthly),
+            paywallComponents = Offering.PaywallComponents(UiConfig(), data),
+        )
+
+        // Act
+        val validated = offering.validatedPaywall(TestData.Constants.currentColorScheme, MockResourceProvider())
+
+        // Assert
+        assertNull(validated.errors)
+    }
 }
diff --git a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/LoadedPaywallComponentsLocaleTests.kt b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/LoadedPaywallComponentsLocaleTests.kt
index 1aab4fe80f..ba6507e108 100644
--- a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/LoadedPaywallComponentsLocaleTests.kt
+++ b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/LoadedPaywallComponentsLocaleTests.kt
@@ -25,6 +25,7 @@ import com.revenuecat.purchases.paywalls.components.common.PaywallComponentsConf
 import com.revenuecat.purchases.paywalls.components.common.PaywallComponentsData
 import com.revenuecat.purchases.paywalls.components.properties.ColorInfo
 import com.revenuecat.purchases.paywalls.components.properties.ColorScheme
+import com.revenuecat.purchases.ui.revenuecatui.data.testdata.TestData
 import com.revenuecat.purchases.ui.revenuecatui.extensions.toComponentsPaywallState
 import com.revenuecat.purchases.ui.revenuecatui.extensions.validatePaywallComponentsDataOrNull
 import com.revenuecat.purchases.ui.revenuecatui.helpers.UiConfig
@@ -58,7 +59,8 @@ internal class LoadedPaywallComponentsLocaleTests {
                             TextComponent(
                                 text = localizationKey,
                                 color = ColorScheme(light = ColorInfo.Hex(Color.Black.toArgb()))
-                            )
+                            ),
+                            TestData.Components.monthlyPackageComponent,
                         )
                     ),
                     background = Background.Color(ColorScheme(light = ColorInfo.Hex(Color.White.toArgb()))),
@@ -79,7 +81,7 @@ internal class LoadedPaywallComponentsLocaleTests {
             identifier = "identifier",
             serverDescription = "serverDescription",
             metadata = emptyMap(),
-            availablePackages = emptyList(),
+            availablePackages = listOf(TestData.Packages.monthly),
             paywallComponents = Offering.PaywallComponents(UiConfig(), paywallComponents),
         )
     }
diff --git a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/PaywallActionTests.kt b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/PaywallActionTests.kt
index 52107000e0..d1ac75c134 100644
--- a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/PaywallActionTests.kt
+++ b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/PaywallActionTests.kt
@@ -29,6 +29,7 @@ import com.revenuecat.purchases.paywalls.components.properties.ColorScheme
 import com.revenuecat.purchases.ui.revenuecatui.InternalPaywall
 import com.revenuecat.purchases.ui.revenuecatui.PaywallOptions
 import com.revenuecat.purchases.ui.revenuecatui.data.testdata.MockViewModel
+import com.revenuecat.purchases.ui.revenuecatui.data.testdata.TestData
 import com.revenuecat.purchases.ui.revenuecatui.helpers.UiConfig
 import com.revenuecat.purchases.ui.revenuecatui.helpers.nonEmptyMapOf
 import org.junit.Assert.assertEquals
@@ -148,7 +149,7 @@ class PaywallActionTests {
         assetBaseURL = URL("https://assets.pawwalls.com"),
         componentsConfig = ComponentsConfig(
             base = PaywallComponentsConfig(
-                stack = StackComponent(components = components),
+                stack = StackComponent(components = components + listOf(TestData.Components.monthlyPackageComponent)),
                 background = Background.Color(ColorScheme(light = ColorInfo.Hex(Color.White.toArgb()))),
                 stickyFooter = StickyFooterComponent(stack = StackComponent(components = components)),
             ),
@@ -163,7 +164,7 @@ class PaywallActionTests {
             identifier = "identifier",
             serverDescription = "description",
             metadata = emptyMap(),
-            availablePackages = emptyList(),
+            availablePackages = listOf(TestData.Packages.monthly),
             paywallComponents = Offering.PaywallComponents(UiConfig(), data),
         )
 
diff --git a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/button/ButtonComponentViewTests.kt b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/button/ButtonComponentViewTests.kt
index 0d70f1d2b5..d49e9f4dda 100644
--- a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/button/ButtonComponentViewTests.kt
+++ b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/button/ButtonComponentViewTests.kt
@@ -124,7 +124,7 @@ class ButtonComponentViewTests {
             )
             ButtonComponentView(
                 style = style,
-                state = FakePaywallState(),
+                state = FakePaywallState(TestData.Packages.annual),
                 onClick = {
                     actionHandleCalledCount++
                     completable.await()
@@ -185,7 +185,8 @@ class ButtonComponentViewTests {
         val state = FakePaywallState(
             localizations = localizations,
             defaultLocaleIdentifier = localeIdEnUs,
-            component
+            packages = listOf(TestData.Packages.monthly),
+            components = listOf(component)
         )
 
         // Act
@@ -264,6 +265,7 @@ class ButtonComponentViewTests {
                     ),
                 ),
             )
+            val packages = listOf(TestData.Packages.annual, TestData.Packages.monthly)
             val styleFactory = StyleFactory(
                 localizations = nonEmptyMapOf(
                     LocaleId("en_US") to nonEmptyMapOf(
@@ -275,11 +277,11 @@ class ButtonComponentViewTests {
                     identifier = "identifier",
                     serverDescription = "description",
                     metadata = emptyMap(),
-                    availablePackages = listOf(TestData.Packages.annual, TestData.Packages.monthly),
+                    availablePackages = packages,
                 )
             )
             val style = styleFactory.create(stackComponent).getOrThrow().componentStyle as StackComponentStyle
-            val state = FakePaywallState()
+            val state = FakePaywallState(packages = packages)
 
             // Act
             var expectedPackageId: String? = null
@@ -353,6 +355,7 @@ class ButtonComponentViewTests {
                     ),
                 ),
             )
+            val packages = listOf(TestData.Packages.annual, TestData.Packages.monthly)
             val styleFactory = StyleFactory(
                 localizations = nonEmptyMapOf(
                     LocaleId("en_US") to nonEmptyMapOf(
@@ -365,11 +368,11 @@ class ButtonComponentViewTests {
                     identifier = "identifier",
                     serverDescription = "description",
                     metadata = emptyMap(),
-                    availablePackages = listOf(TestData.Packages.annual, TestData.Packages.monthly),
+                    availablePackages = packages,
                 )
             )
             val style = styleFactory.create(stackComponent).getOrThrow().componentStyle as StackComponentStyle
-            val state = FakePaywallState()
+            val state = FakePaywallState(packages = packages)
 
             // Act
             setContent {
diff --git a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/stack/StackComponentViewTests.kt b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/stack/StackComponentViewTests.kt
index 60dc328bf3..e92f4b9d11 100644
--- a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/stack/StackComponentViewTests.kt
+++ b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/stack/StackComponentViewTests.kt
@@ -94,7 +94,14 @@ class StackComponentViewTests {
                 dark = ColorInfo.Hex(expectedDarkColor.toArgb()),
             ),
         )
-        val state = FakePaywallState(component)
+        val state = FakePaywallState(
+            components = listOf(component),
+            packages = listOf(
+                packageWithoutIntroOffer,
+                packageWithSingleIntroOffer,
+                packageWithMultipleIntroOffers
+            )
+        )
 
         themeChangingTest(
             arrange = {
@@ -143,7 +150,14 @@ class StackComponentViewTests {
                 width = borderWidthDp
             ),
         )
-        val state = FakePaywallState(component)
+        val state = FakePaywallState(
+            components = listOf(component),
+            packages = listOf(
+                packageWithoutIntroOffer,
+                packageWithSingleIntroOffer,
+                packageWithMultipleIntroOffers
+            )
+        )
 
         themeChangingTest(
             arrange = {
@@ -202,7 +216,14 @@ class StackComponentViewTests {
             ),
             backgroundColor = ColorScheme(light = ColorInfo.Hex(expectedBackgroundColor.toArgb())),
         )
-        val state = FakePaywallState(component)
+        val state = FakePaywallState(
+            components = listOf(component),
+            packages = listOf(
+                packageWithoutIntroOffer,
+                packageWithSingleIntroOffer,
+                packageWithMultipleIntroOffers
+            )
+        )
 
         themeChangingTest(
             arrange = {
diff --git a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/stack/StackComponentViewWindowTests.kt b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/stack/StackComponentViewWindowTests.kt
index 88358d2687..16be33655c 100644
--- a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/stack/StackComponentViewWindowTests.kt
+++ b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/stack/StackComponentViewWindowTests.kt
@@ -35,6 +35,7 @@ import com.revenuecat.purchases.ui.revenuecatui.assertions.assertApproximatePixe
 import com.revenuecat.purchases.ui.revenuecatui.assertions.assertNoPixelColorEquals
 import com.revenuecat.purchases.ui.revenuecatui.assertions.assertRectangularBorderColor
 import com.revenuecat.purchases.ui.revenuecatui.components.style.StackComponentStyle
+import com.revenuecat.purchases.ui.revenuecatui.data.testdata.TestData
 import com.revenuecat.purchases.ui.revenuecatui.helpers.FakePaywallState
 import com.revenuecat.purchases.ui.revenuecatui.helpers.StyleFactory
 import com.revenuecat.purchases.ui.revenuecatui.helpers.getOrThrow
@@ -130,7 +131,7 @@ internal class StackComponentViewWindowTests {
                 ),
             )
         )
-        val state = FakePaywallState(component)
+        val state = FakePaywallState(packages = listOf(TestData.Packages.monthly), components = listOf(component))
         val styleFactory = StyleFactory()
         val style = styleFactory.create(component).getOrThrow().componentStyle as StackComponentStyle
         val content = @Composable {
diff --git a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/tabs/TabsComponentViewTests.kt b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/tabs/TabsComponentViewTests.kt
index cbd0dd9c98..72cdc770c0 100644
--- a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/tabs/TabsComponentViewTests.kt
+++ b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/tabs/TabsComponentViewTests.kt
@@ -139,7 +139,10 @@ class TabsComponentViewTests {
             assetBaseURL = URL("https://assets.pawwalls.com"),
             componentsConfig = ComponentsConfig(
                 base = PaywallComponentsConfig(
-                    stack = StackComponent(components = listOf(tabsComponent)),
+                    stack = StackComponent(components = listOf(
+                        tabsComponent,
+                        TestData.Components.monthlyPackageComponent,
+                    )),
                     background = Background.Color(ColorScheme(light = ColorInfo.Hex(Color.White.toArgb()))),
                     stickyFooter = null,
                 ),
@@ -151,7 +154,7 @@ class TabsComponentViewTests {
             identifier = "offering-id",
             serverDescription = "description",
             metadata = emptyMap(),
-            availablePackages = emptyList(),
+            availablePackages = listOf(TestData.Packages.monthly),
             paywallComponents = Offering.PaywallComponents(UiConfig(), data),
         )
         val validated = offering.validatePaywallComponentsDataOrNull()?.getOrThrow()!!
@@ -312,7 +315,10 @@ class TabsComponentViewTests {
             assetBaseURL = URL("https://assets.pawwalls.com"),
             componentsConfig = ComponentsConfig(
                 base = PaywallComponentsConfig(
-                    stack = StackComponent(components = listOf(tabsComponent)),
+                    stack = StackComponent(components = listOf(
+                        tabsComponent,
+                        TestData.Components.monthlyPackageComponent,
+                    )),
                     background = Background.Color(ColorScheme(light = ColorInfo.Hex(Color.White.toArgb()))),
                     stickyFooter = null,
                 ),
@@ -324,7 +330,7 @@ class TabsComponentViewTests {
             identifier = "offering-id",
             serverDescription = "description",
             metadata = emptyMap(),
-            availablePackages = emptyList(),
+            availablePackages = listOf(TestData.Packages.monthly),
             paywallComponents = Offering.PaywallComponents(UiConfig(), data),
         )
         val validated = offering.validatePaywallComponentsDataOrNull()?.getOrThrow()!!
diff --git a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/text/TextComponentViewTests.kt b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/text/TextComponentViewTests.kt
index 7f97390e52..90b48dbdc4 100644
--- a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/text/TextComponentViewTests.kt
+++ b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/text/TextComponentViewTests.kt
@@ -173,7 +173,8 @@ class TextComponentViewTests {
         val state = FakePaywallState(
             localizations = localizations,
             defaultLocaleIdentifier = localeIdEnUs,
-            component
+            packages = listOf(TestData.Packages.monthly),
+            components = listOf(component),
         )
 
         themeChangingTest(
@@ -218,7 +219,8 @@ class TextComponentViewTests {
         val state = FakePaywallState(
             localizations = localizations,
             defaultLocaleIdentifier = localeIdEnUs,
-            component
+            packages = listOf(TestData.Packages.monthly),
+            components = listOf(component),
         )
 
         themeChangingTest(
@@ -259,8 +261,8 @@ class TextComponentViewTests {
         val state = FakePaywallState(
             localizations = localizations,
             defaultLocaleIdentifier = localeIdEnUs,
-            largeTextComponent,
-            smallTextComponent
+            packages = listOf(TestData.Packages.monthly),
+            components = listOf(largeTextComponent, smallTextComponent),
         )
         setContent {
             val largeTextStyle =
@@ -615,7 +617,8 @@ class TextComponentViewTests {
         val state = FakePaywallState(
             localizations = localizations,
             defaultLocaleIdentifier = localeIdEnUs,
-            component
+            packages = listOf(TestData.Packages.monthly),
+            components = listOf(component),
         )
 
         // Act
@@ -696,12 +699,20 @@ class TextComponentViewTests {
             text = selectedPackageTextKey,
             color = textColor,
         )
+        val packageComponent = PackageComponent(
+            packageId = packageYearly.identifier,
+            isSelectedByDefault = true,
+            stack = StackComponent(components = emptyList()),
+        )
         val data = PaywallComponentsData(
             templateName = "template",
             assetBaseURL = URL("https://assets.pawwalls.com"),
             componentsConfig = ComponentsConfig(
                 base = PaywallComponentsConfig(
-                    stack = StackComponent(components = listOf(selectedComponent)),
+                    stack = StackComponent(components = listOf(
+                        selectedComponent,
+                        packageComponent,
+                    )),
                     background = Background.Color(ColorScheme(light = ColorInfo.Hex(Color.White.toArgb()))),
                     stickyFooter = null,
                 ),
@@ -772,12 +783,17 @@ class TextComponentViewTests {
             )
         )
         val component = TextComponent(text = textKey, color = textColor)
+        val packageComponent = PackageComponent(
+            packageId = usdPackage.identifier,
+            isSelectedByDefault = false,
+            stack = StackComponent(components = emptyList()),
+        )
         val data = PaywallComponentsData(
             templateName = "template",
             assetBaseURL = URL("https://assets.pawwalls.com"),
             componentsConfig = ComponentsConfig(
                 base = PaywallComponentsConfig(
-                    stack = StackComponent(components = listOf(component)),
+                    stack = StackComponent(components = listOf(component, packageComponent)),
                     background = Background.Color(ColorScheme(light = ColorInfo.Hex(Color.White.toArgb()))),
                     stickyFooter = null,
                 ),
diff --git a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/text/TextComponentViewWindowTests.kt b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/text/TextComponentViewWindowTests.kt
index 17d1d97b51..985d672797 100644
--- a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/text/TextComponentViewWindowTests.kt
+++ b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/text/TextComponentViewWindowTests.kt
@@ -26,6 +26,7 @@ import com.revenuecat.purchases.paywalls.components.properties.ColorScheme
 import com.revenuecat.purchases.ui.revenuecatui.assertions.assertPixelColorPercentage
 import com.revenuecat.purchases.ui.revenuecatui.assertions.assertTextColorEquals
 import com.revenuecat.purchases.ui.revenuecatui.components.style.TextComponentStyle
+import com.revenuecat.purchases.ui.revenuecatui.data.testdata.TestData
 import com.revenuecat.purchases.ui.revenuecatui.helpers.FakePaywallState
 import com.revenuecat.purchases.ui.revenuecatui.helpers.StyleFactory
 import com.revenuecat.purchases.ui.revenuecatui.helpers.getOrThrow
@@ -108,7 +109,8 @@ internal class TextComponentViewWindowTests {
         val state = FakePaywallState(
             localizations = localizations,
             defaultLocaleIdentifier = localeId,
-            component
+            packages = listOf(TestData.Packages.monthly),
+            components = listOf(component),
         )
         val styleFactory = StyleFactory(
             localizations = localizations,
diff --git a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/MostExpensivePricePerMonthMicrosTests.kt b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/MostExpensivePricePerMonthMicrosTests.kt
index c5282abaa8..931fcc7079 100644
--- a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/MostExpensivePricePerMonthMicrosTests.kt
+++ b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/MostExpensivePricePerMonthMicrosTests.kt
@@ -5,6 +5,8 @@ import com.revenuecat.purchases.PackageType
 import com.revenuecat.purchases.models.Period
 import com.revenuecat.purchases.models.Price
 import com.revenuecat.purchases.models.TestStoreProduct
+import com.revenuecat.purchases.paywalls.components.PackageComponent
+import com.revenuecat.purchases.paywalls.components.StackComponent
 import com.revenuecat.purchases.ui.revenuecatui.helpers.FakePaywallState
 import org.assertj.core.api.Assertions.assertThat
 import org.junit.Test
@@ -13,18 +15,6 @@ import java.util.Locale
 
 class MostExpensivePricePerMonthMicrosTests {
 
-    @Test
-    fun `mostExpensivePricePerMonthMicros should be null for empty package list`() {
-        // Arrange
-        val packages = emptyList<Package>()
-
-        // Act
-        val actual = FakePaywallState(packages = packages).mostExpensivePricePerMonthMicros
-
-        // Assert
-        assertThat(actual).isNull()
-    }
-
     @Test
     fun `mostExpensivePricePerMonthMicros should return correct price for single package`() {
         // Arrange
@@ -46,8 +36,27 @@ class MostExpensivePricePerMonthMicrosTests {
         val package3 = monthlyPackageWithPrice(500_000, identifier = "3")
         val expected = 2_000_000L
 
+        val package1Component = PackageComponent(
+            packageId = package1.identifier,
+            isSelectedByDefault = false,
+            stack = StackComponent(components = emptyList())
+        )
+        val package2Component = PackageComponent(
+            packageId = package2.identifier,
+            isSelectedByDefault = false,
+            stack = StackComponent(components = emptyList())
+        )
+        val package3Component = PackageComponent(
+            packageId = package3.identifier,
+            isSelectedByDefault = false,
+            stack = StackComponent(components = emptyList())
+        )
+
         // Act
-        val actual = FakePaywallState(package1, package2, package3).mostExpensivePricePerMonthMicros
+        val actual = FakePaywallState(
+            packages = listOf(package1, package2, package3),
+            components = listOf(package1Component, package2Component, package3Component),
+        ).mostExpensivePricePerMonthMicros
 
         // Assert
         assertThat(actual).isEqualTo(expected)
@@ -89,8 +98,22 @@ class MostExpensivePricePerMonthMicrosTests {
         val package1 = lifetimePackageWithPrice(1_000_000_000)
         val package2 = lifetimePackageWithPrice(2_000_000_000)
 
+        val package1Component = PackageComponent(
+            packageId = package1.identifier,
+            isSelectedByDefault = false,
+            stack = StackComponent(components = emptyList())
+        )
+        val package2Component = PackageComponent(
+            packageId = package2.identifier,
+            isSelectedByDefault = false,
+            stack = StackComponent(components = emptyList())
+        )
+
         // Act
-        val actual = FakePaywallState(package1, package2).mostExpensivePricePerMonthMicros
+        val actual = FakePaywallState(
+            packages = listOf(package1, package2),
+            components = listOf(package1Component, package2Component),
+        ).mostExpensivePricePerMonthMicros
 
         // Assert
         assertThat(actual).isNull()
diff --git a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModelTest.kt b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModelTest.kt
index d76f6e9a1d..cf53235a42 100644
--- a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModelTest.kt
+++ b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModelTest.kt
@@ -85,7 +85,8 @@ class PaywallViewModelTest {
         assetBaseURL = URL("https://assets.pawwalls.com"),
         componentsConfig = ComponentsConfig(
             base = PaywallComponentsConfig(
-                stack = StackComponent(components = emptyList()),
+                // Need to have at least one PackageComponent for the PaywallState to be valid.
+                stack = StackComponent(components = listOf(TestData.Components.monthlyPackageComponent)),
                 background = Background.Color(ColorScheme(light = ColorInfo.Hex(Color.White.toArgb()))),
                 stickyFooter = null,
             ),
diff --git a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/helpers/FakePaywallState.kt b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/helpers/FakePaywallState.kt
index 88d09a2da0..bc88c26cc4 100644
--- a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/helpers/FakePaywallState.kt
+++ b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/helpers/FakePaywallState.kt
@@ -20,6 +20,7 @@ import com.revenuecat.purchases.paywalls.components.properties.ColorInfo
 import com.revenuecat.purchases.paywalls.components.properties.ColorScheme
 import com.revenuecat.purchases.ui.revenuecatui.components.ktx.LocalizationDictionary
 import com.revenuecat.purchases.ui.revenuecatui.data.PaywallState
+import com.revenuecat.purchases.ui.revenuecatui.data.testdata.TestData
 import com.revenuecat.purchases.ui.revenuecatui.extensions.toComponentsPaywallState
 import com.revenuecat.purchases.ui.revenuecatui.extensions.validatePaywallComponentsDataOrNull
 import java.net.URL


Reviews:
user: login: joshdholtz
body: Looks good to me! One comment about commented out code but otherwise :shipit: 
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-29T17:03:24Z
user: login: tonidero
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-29T17:04:07Z
user: login: tonidero
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-29T17:05:53Z

Review comments:
diff_hunk: @@ -0,0 +1,24 @@
+package com.revenuecat.purchases.ui.revenuecatui
+
+import com.revenuecat.purchases.Package
+import com.revenuecat.purchases.PackageType
+import com.revenuecat.purchases.paywalls.components.PackageComponent
+import com.revenuecat.purchases.paywalls.components.StackComponent
+import io.mockk.mockk
+
+object FakeData {
+//    private const val FAKE_PACKAGE_ID = "fake_package"
+//
+//    val fakePackage = Package(
+//        identifier = FAKE_PACKAGE_ID,
+//        packageType = PackageType.CUSTOM,
+//        product = mockk(),
+//        presentedOfferingContext = mockk(),
+//    )
+//
+//    val packageComponent = PackageComponent(
+//        packageId = FAKE_PACKAGE_ID,
+//        isSelectedByDefault = false,
+//        stack = StackComponent(components = emptyList())
+//    )
path: ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/FakeData.kt
user: login: joshdholtz
body: Do we want to delete this? 🤔 
created_at: 2025-05-29T17:00:58Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,24 @@
+package com.revenuecat.purchases.ui.revenuecatui
+
+import com.revenuecat.purchases.Package
+import com.revenuecat.purchases.PackageType
+import com.revenuecat.purchases.paywalls.components.PackageComponent
+import com.revenuecat.purchases.paywalls.components.StackComponent
+import io.mockk.mockk
+
+object FakeData {
+//    private const val FAKE_PACKAGE_ID = "fake_package"
+//
+//    val fakePackage = Package(
+//        identifier = FAKE_PACKAGE_ID,
+//        packageType = PackageType.CUSTOM,
+//        product = mockk(),
+//        presentedOfferingContext = mockk(),
+//    )
+//
+//    val packageComponent = PackageComponent(
+//        packageId = FAKE_PACKAGE_ID,
+//        isSelectedByDefault = false,
+//        stack = StackComponent(components = emptyList())
+//    )
path: ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/FakeData.kt
user: login: tonidero
body: Oops thanks!
created_at: 2025-05-29T17:04:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -13,18 +15,6 @@ import java.util.Locale
 
 class MostExpensivePricePerMonthMicrosTests {
 
-    @Test
-    fun `mostExpensivePricePerMonthMicros should be null for empty package list`() {
path: ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/MostExpensivePricePerMonthMicrosTests.kt
user: login: tonidero
body: This test won't work anymore, since we need at least one package for validation to pass.
created_at: 2025-05-29T17:05:52Z
author_association: CONTRIBUTOR

PR comments:
user: login: emerge-tools[bot]
created_at: 2025-05-29T17:00:31Z
body: # 📸 Snapshot Test 
## 447 unchanged
| Name | Added | Removed | Modified | Renamed | Unchanged | Errored | Approval |
| :--- | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
| <a href="https://www.emergetools.com/snapshot/compare/681e0aa4-b168-4404-8132-76b71672845b?utm_source=github&utm_medium=vcs" target="_blank" rel="noreferrer noopener">TestPurchasesUIAndroidCompatibility</a><br>com.revenuecat.testpurchasesuiandroidcompatibility | <a href="https://www.emergetools.com/snapshot/compare/681e0aa4-b168-4404-8132-76b71672845b?tab=added&utm_source=github&utm_medium=vcs" target="_blank" rel="noreferrer noopener">0</a> | <a href="https://www.emergetools.com/snapshot/compare/681e0aa4-b168-4404-8132-76b71672845b?tab=removed&utm_source=github&utm_medium=vcs" target="_blank" rel="noreferrer noopener">0</a> | <a href="https://www.emergetools.com/snapshot/compare/681e0aa4-b168-4404-8132-76b71672845b?tab=changed&utm_source=github&utm_medium=vcs" target="_blank" rel="noreferrer noopener">0</a> | <a href="https://www.emergetools.com/snapshot/compare/681e0aa4-b168-4404-8132-76b71672845b?tab=renamed&utm_source=github&utm_medium=vcs" target="_blank" rel="noreferrer noopener">0</a> | <a href="https://www.emergetools.com/snapshot/compare/681e0aa4-b168-4404-8132-76b71672845b?tab=unchanged&utm_source=github&utm_medium=vcs" target="_blank" rel="noreferrer noopener">290</a> | <a href="https://www.emergetools.com/snapshot/compare/681e0aa4-b168-4404-8132-76b71672845b?tab=errors&utm_source=github&utm_medium=vcs" target="_blank" rel="noreferrer noopener">0</a> | N/A |
| <a href="https://www.emergetools.com/snapshot/compare/771f8a1f-d450-458d-a630-9bca43c45e79?utm_source=github&utm_medium=vcs" target="_blank" rel="noreferrer noopener">TestPurchasesUIAndroidCompatibility Paparazzi</a><br>com.revenuecat.testpurchasesuiandroidcompatibility.paparazzi | <a href="https://www.emergetools.com/snapshot/compare/771f8a1f-d450-458d-a630-9bca43c45e79?tab=added&utm_source=github&utm_medium=vcs" target="_blank" rel="noreferrer noopener">0</a> | <a href="https://www.emergetools.com/snapshot/compare/771f8a1f-d450-458d-a630-9bca43c45e79?tab=removed&utm_source=github&utm_medium=vcs" target="_blank" rel="noreferrer noopener">0</a> | <a href="https://www.emergetools.com/snapshot/compare/771f8a1f-d450-458d-a630-9bca43c45e79?tab=changed&utm_source=github&utm_medium=vcs" target="_blank" rel="noreferrer noopener">0</a> | <a href="https://www.emergetools.com/snapshot/compare/771f8a1f-d450-458d-a630-9bca43c45e79?tab=renamed&utm_source=github&utm_medium=vcs" target="_blank" rel="noreferrer noopener">0</a> | <a href="https://www.emergetools.com/snapshot/compare/771f8a1f-d450-458d-a630-9bca43c45e79?tab=unchanged&utm_source=github&utm_medium=vcs" target="_blank" rel="noreferrer noopener">157</a> | <a href="https://www.emergetools.com/snapshot/compare/771f8a1f-d450-458d-a630-9bca43c45e79?tab=errors&utm_source=github&utm_medium=vcs" target="_blank" rel="noreferrer noopener">0</a> | N/A |


---

:flying_saucer: Powered by <a href="https://www.emergetools.com/?utm_source=github&utm_medium=vcs" target="_blank" rel="noreferrer noopener">Emerge Tools</a>
author_association: NONE
user: login: codecov[bot]
created_at: 2025-05-30T07:41:11Z
body: ## [Codecov](https://app.codecov.io/gh/RevenueCat/purchases-android/pull/2408?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=RevenueCat) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 79.66%. Comparing base [(`1f53624`)](https://app.codecov.io/gh/RevenueCat/purchases-android/commit/1f536242bda7298485eb2169078b5fb214aa6873?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=RevenueCat) to head [(`cd8f97d`)](https://app.codecov.io/gh/RevenueCat/purchases-android/commit/cd8f97d46477aed64ad243ddf70f18d93d07ecf2?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=RevenueCat).
> Report is 1 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main    #2408   +/-   ##
=======================================
  Coverage   79.66%   79.66%           
=======================================
  Files         284      284           
  Lines       10261    10261           
  Branches     1457     1457           
=======================================
  Hits         8174     8174           
  Misses       1460     1460           
  Partials      627      627           
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/RevenueCat/purchases-android/pull/2408?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=RevenueCat).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=RevenueCat).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE