From 15ee579e99c556d6ef1bcb7548c15c030bb7f898 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Wed, 11 Dec 2024 19:03:05 +0100 Subject: [PATCH 01/37] Adds PaywallComponentsData to Offering, and has PaywallViewModel use that. --- .../com/revenuecat/purchases/Offering.kt | 8 +++- .../ui/revenuecatui/data/PaywallViewModel.kt | 46 +++++++++++-------- .../ui/revenuecatui/data/testdata/TestData.kt | 4 +- .../helpers/OfferingToStateMapper.kt | 19 +++++--- .../helpers/PaywallValidationResult.kt | 20 ++++++-- 5 files changed, 64 insertions(+), 33 deletions(-) diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/Offering.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/Offering.kt index 0d0592e8e7..108860ffd5 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/Offering.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/Offering.kt @@ -6,6 +6,7 @@ package com.revenuecat.purchases import com.revenuecat.purchases.paywalls.PaywallData +import com.revenuecat.purchases.paywalls.components.common.PaywallComponentsData /** * An offering is a collection of [Package] available for the user to purchase. @@ -15,12 +16,17 @@ import com.revenuecat.purchases.paywalls.PaywallData * @property availablePackages Array of [Package] objects available for purchase. * @property metadata Offering metadata defined in RevenueCat dashboard. */ -data class Offering @JvmOverloads constructor( +data class Offering +@OptIn(InternalRevenueCatAPI::class) +@JvmOverloads +constructor( val identifier: String, val serverDescription: String, val metadata: Map, val availablePackages: List, val paywall: PaywallData? = null, + @InternalRevenueCatAPI + val paywallComponents: PaywallComponentsData? = null, ) { /** diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModel.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModel.kt index 71b4a51940..fb1eb43244 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModel.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModel.kt @@ -27,9 +27,11 @@ import com.revenuecat.purchases.ui.revenuecatui.PurchaseLogicResult import com.revenuecat.purchases.ui.revenuecatui.data.processed.TemplateConfiguration import com.revenuecat.purchases.ui.revenuecatui.data.processed.VariableDataProvider import com.revenuecat.purchases.ui.revenuecatui.helpers.Logger +import com.revenuecat.purchases.ui.revenuecatui.helpers.PaywallValidationResult import com.revenuecat.purchases.ui.revenuecatui.helpers.ResourceProvider -import com.revenuecat.purchases.ui.revenuecatui.helpers.toPaywallState -import com.revenuecat.purchases.ui.revenuecatui.helpers.validatedPaywall +import com.revenuecat.purchases.ui.revenuecatui.helpers.toComponentsPaywallState +import com.revenuecat.purchases.ui.revenuecatui.helpers.toLegacyPaywallState +import com.revenuecat.purchases.ui.revenuecatui.helpers.validatedLegacyPaywall import com.revenuecat.purchases.ui.revenuecatui.strings.PaywallValidationErrorStrings import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -371,28 +373,34 @@ internal class PaywallViewModelImpl( if (offering.availablePackages.isEmpty()) { return PaywallState.Error("No packages available") } - val (displayablePaywall, template, error) = offering.validatedPaywall( - colorScheme, - resourceProvider, - ) - error?.let { validationError -> + val validationResult = offering.paywallComponents + // TODO Actually validate the PaywallComponentsData + ?.let { PaywallValidationResult.Components(displayablePaywall = it) } + ?: offering.validatedLegacyPaywall(colorScheme, resourceProvider) + + validationResult.error?.let { validationError -> Logger.w(validationError.associatedErrorString(offering)) Logger.w(PaywallValidationErrorStrings.DISPLAYING_DEFAULT) } - return offering.toPaywallState( - variableDataProvider = variableDataProvider, - activelySubscribedProductIdentifiers = customerInfo.activeSubscriptions, - nonSubscriptionProductIdentifiers = customerInfo.nonSubscriptionTransactions - .map { it.productIdentifier } - .toSet(), - mode = mode, - validatedPaywallData = displayablePaywall, - template = template, - shouldDisplayDismissButton = options.shouldDisplayDismissButton, - storefrontCountryCode = storefrontCountryCode, - ) + return when (validationResult) { + is PaywallValidationResult.Legacy -> offering.toLegacyPaywallState( + variableDataProvider = variableDataProvider, + activelySubscribedProductIdentifiers = customerInfo.activeSubscriptions, + nonSubscriptionProductIdentifiers = customerInfo.nonSubscriptionTransactions + .map { it.productIdentifier } + .toSet(), + mode = mode, + validatedPaywallData = validationResult.displayablePaywall, + template = validationResult.template, + shouldDisplayDismissButton = options.shouldDisplayDismissButton, + storefrontCountryCode = storefrontCountryCode, + ) + is PaywallValidationResult.Components -> offering.toComponentsPaywallState( + validatedPaywallData = validationResult.displayablePaywall, + ) + } } /** 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 101c9b0988..0a74fc333c 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 @@ -31,7 +31,7 @@ import com.revenuecat.purchases.ui.revenuecatui.data.testdata.templates.template import com.revenuecat.purchases.ui.revenuecatui.data.testdata.templates.template7 import com.revenuecat.purchases.ui.revenuecatui.data.testdata.templates.template7CustomPackages import com.revenuecat.purchases.ui.revenuecatui.helpers.ResourceProvider -import com.revenuecat.purchases.ui.revenuecatui.helpers.toPaywallState +import com.revenuecat.purchases.ui.revenuecatui.helpers.toLegacyPaywallState import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -441,7 +441,7 @@ internal class MockViewModel( } private val _state = MutableStateFlow( - offering.toPaywallState( + offering.toLegacyPaywallState( variableDataProvider = VariableDataProvider(resourceProvider), activelySubscribedProductIdentifiers = setOf(), nonSubscriptionProductIdentifiers = setOf(), 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 50ac5b1abc..e561777f53 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 @@ -3,6 +3,7 @@ package com.revenuecat.purchases.ui.revenuecatui.helpers import androidx.compose.material3.ColorScheme import com.revenuecat.purchases.Offering import com.revenuecat.purchases.paywalls.PaywallData +import com.revenuecat.purchases.paywalls.components.common.PaywallComponentsData import com.revenuecat.purchases.ui.revenuecatui.PaywallMode import com.revenuecat.purchases.ui.revenuecatui.composables.PaywallIconName import com.revenuecat.purchases.ui.revenuecatui.data.PaywallState @@ -18,12 +19,12 @@ import com.revenuecat.purchases.ui.revenuecatui.extensions.defaultTemplate import kotlin.Result @Suppress("ReturnCount") -internal fun Offering.validatedPaywall( +internal fun Offering.validatedLegacyPaywall( currentColorScheme: ColorScheme, resourceProvider: ResourceProvider, -): PaywallValidationResult { +): PaywallValidationResult.Legacy { val paywallData = this.paywall - ?: return PaywallValidationResult( + ?: return PaywallValidationResult.Legacy( PaywallData.createDefault( availablePackages, currentColorScheme, @@ -34,7 +35,7 @@ internal fun Offering.validatedPaywall( ) val template = paywallData.validate().getOrElse { - return PaywallValidationResult( + return PaywallValidationResult.Legacy( PaywallData.createDefaultForIdentifiers( paywallData.config.packageIds, currentColorScheme, @@ -44,7 +45,7 @@ internal fun Offering.validatedPaywall( it as PaywallValidationError, ) } - return PaywallValidationResult( + return PaywallValidationResult.Legacy( paywallData, template, ) @@ -129,7 +130,7 @@ private fun PaywallData.LocalizedConfiguration.validate(): PaywallValidationErro } @Suppress("ReturnCount", "TooGenericExceptionCaught", "LongParameterList") -internal fun Offering.toPaywallState( +internal fun Offering.toLegacyPaywallState( variableDataProvider: VariableDataProvider, activelySubscribedProductIdentifiers: Set, nonSubscriptionProductIdentifiers: Set, @@ -161,6 +162,12 @@ internal fun Offering.toPaywallState( ) } +internal fun Offering.toComponentsPaywallState(validatedPaywallData: PaywallComponentsData): PaywallState = + PaywallState.Loaded.Components( + offering = this, + data = validatedPaywallData, + ) + /** * Returns an error if any of the variables are invalid, or null if they're all valid */ diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/helpers/PaywallValidationResult.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/helpers/PaywallValidationResult.kt index d2b54770b6..2ab9ea5eac 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/helpers/PaywallValidationResult.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/helpers/PaywallValidationResult.kt @@ -1,11 +1,21 @@ package com.revenuecat.purchases.ui.revenuecatui.helpers import com.revenuecat.purchases.paywalls.PaywallData +import com.revenuecat.purchases.paywalls.components.common.PaywallComponentsData import com.revenuecat.purchases.ui.revenuecatui.data.processed.PaywallTemplate import com.revenuecat.purchases.ui.revenuecatui.errors.PaywallValidationError -internal data class PaywallValidationResult( - val displayablePaywall: PaywallData, - val template: PaywallTemplate, - val error: PaywallValidationError? = null, -) +internal sealed interface PaywallValidationResult { + val error: PaywallValidationError? + + data class Legacy( + val displayablePaywall: PaywallData, + val template: PaywallTemplate, + override val error: PaywallValidationError? = null, + ) : PaywallValidationResult + + data class Components( + val displayablePaywall: PaywallComponentsData, + override val error: PaywallValidationError? = null, + ) : PaywallValidationResult +} From 97a6d2a1acd40606c9c5dc2f5090465314ac8b1d Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Thu, 12 Dec 2024 11:57:01 +0100 Subject: [PATCH 02/37] Renames PaywallDataValidationTest to LegacyPaywallDataValidationTest. --- ...ValidationTest.kt => LegacyPaywallDataValidationTest.kt} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/{PaywallDataValidationTest.kt => LegacyPaywallDataValidationTest.kt} (99%) diff --git a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/PaywallDataValidationTest.kt b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/LegacyPaywallDataValidationTest.kt similarity index 99% rename from ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/PaywallDataValidationTest.kt rename to ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/LegacyPaywallDataValidationTest.kt index 682d6af9a5..a80502d9f3 100644 --- a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/PaywallDataValidationTest.kt +++ b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/LegacyPaywallDataValidationTest.kt @@ -7,7 +7,7 @@ import com.revenuecat.purchases.ui.revenuecatui.data.testdata.MockResourceProvid import com.revenuecat.purchases.ui.revenuecatui.data.testdata.TestData import com.revenuecat.purchases.ui.revenuecatui.errors.PaywallValidationError import com.revenuecat.purchases.ui.revenuecatui.helpers.Logger -import com.revenuecat.purchases.ui.revenuecatui.helpers.validatedPaywall +import com.revenuecat.purchases.ui.revenuecatui.helpers.validatedLegacyPaywall import io.mockk.mockkObject import io.mockk.verify import kotlinx.serialization.json.Json @@ -17,7 +17,7 @@ import org.junit.runner.RunWith import java.io.File @RunWith(AndroidJUnit4::class) -class PaywallDataValidationTest { +class LegacyPaywallDataValidationTest { @Test fun `Validate an offering without paywall`() { @@ -231,7 +231,7 @@ class PaywallDataValidationTest { ) } - private fun getPaywallValidationResult(offering: Offering) = offering.validatedPaywall( + private fun getPaywallValidationResult(offering: Offering) = offering.validatedLegacyPaywall( currentColorScheme = TestData.Constants.currentColorScheme, resourceProvider = MockResourceProvider() ) From f039302d19523a42a4f910c0c58605c92e0f45bb Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Thu, 12 Dec 2024 11:57:39 +0100 Subject: [PATCH 03/37] LoadingPaywall calls toLegacyPaywallState. --- .../revenuecat/purchases/ui/revenuecatui/LoadingPaywall.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/LoadingPaywall.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/LoadingPaywall.kt index e506b31190..2bf6dda11c 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/LoadingPaywall.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/LoadingPaywall.kt @@ -33,7 +33,7 @@ import com.revenuecat.purchases.ui.revenuecatui.data.processed.VariableDataProvi import com.revenuecat.purchases.ui.revenuecatui.extensions.createDefault import com.revenuecat.purchases.ui.revenuecatui.helpers.ResourceProvider import com.revenuecat.purchases.ui.revenuecatui.helpers.isInPreviewMode -import com.revenuecat.purchases.ui.revenuecatui.helpers.toPaywallState +import com.revenuecat.purchases.ui.revenuecatui.helpers.toLegacyPaywallState import com.revenuecat.purchases.ui.revenuecatui.helpers.toResourceProvider import com.revenuecat.purchases.ui.revenuecatui.templates.Template2 import kotlinx.coroutines.flow.MutableStateFlow @@ -62,7 +62,7 @@ internal fun LoadingPaywall( paywall = paywallData, ) - val state = offering.toPaywallState( + val state = offering.toLegacyPaywallState( variableDataProvider = VariableDataProvider( resourceProvider, isInPreviewMode(), From 9153fc03a29fac2f27d24ffe275cbde8679327dd Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Thu, 12 Dec 2024 11:59:35 +0100 Subject: [PATCH 04/37] OfferingParser parses paywall_components. --- .../purchases/common/OfferingParser.kt | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/common/OfferingParser.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/common/OfferingParser.kt index ab0838be33..661354f2d3 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/common/OfferingParser.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/common/OfferingParser.kt @@ -1,6 +1,7 @@ package com.revenuecat.purchases.common import androidx.annotation.VisibleForTesting +import com.revenuecat.purchases.InternalRevenueCatAPI import com.revenuecat.purchases.Offering import com.revenuecat.purchases.Offerings import com.revenuecat.purchases.Package @@ -8,6 +9,7 @@ import com.revenuecat.purchases.PackageType import com.revenuecat.purchases.PresentedOfferingContext import com.revenuecat.purchases.models.StoreProduct import com.revenuecat.purchases.paywalls.PaywallData +import com.revenuecat.purchases.paywalls.components.common.PaywallComponentsData import com.revenuecat.purchases.strings.OfferingStrings import com.revenuecat.purchases.utils.getNullableString import com.revenuecat.purchases.utils.optNullableInt @@ -15,7 +17,6 @@ import com.revenuecat.purchases.utils.optNullableString import com.revenuecat.purchases.utils.replaceJsonNullWithKotlinNull import com.revenuecat.purchases.utils.toMap import com.revenuecat.purchases.withPresentedContext -import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import org.json.JSONObject @@ -90,6 +91,7 @@ internal abstract class OfferingParser { ) } + @OptIn(InternalRevenueCatAPI::class) @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) fun createOffering(offeringJson: JSONObject, productsById: Map>): Offering? { val offeringIdentifier = offeringJson.getString("identifier") @@ -117,6 +119,18 @@ internal abstract class OfferingParser { } } + val paywallComponentsDataJson = offeringJson.optJSONObject("paywall_components") + + @Suppress("TooGenericExceptionCaught") + val paywallComponentsData: PaywallComponentsData? = paywallComponentsDataJson?.let { + try { + json.decodeFromString(it.toString()) + } catch (e: Throwable) { + errorLog("Error deserializing paywall components data", e) + null + } + } + return if (availablePackages.isNotEmpty()) { Offering( offeringIdentifier, @@ -124,6 +138,7 @@ internal abstract class OfferingParser { metadata, availablePackages, paywallData, + paywallComponentsData, ) } else { null From 0a07fd501bb6acf8e20cd21c170f3fda443a7618 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Thu, 12 Dec 2024 12:02:29 +0100 Subject: [PATCH 05/37] Adds Paywalls V2 support to PaywallsTester. --- .../revenuecat/paywallstester/Constants.kt | 2 +- .../paywallstester/SamplePaywalls.kt | 260 ++++++++++++++++-- .../screens/main/offerings/OfferingsScreen.kt | 10 +- .../screens/main/paywalls/PaywallsScreen.kt | 75 ++--- 4 files changed, 279 insertions(+), 68 deletions(-) diff --git a/examples/paywall-tester/src/main/java/com/revenuecat/paywallstester/Constants.kt b/examples/paywall-tester/src/main/java/com/revenuecat/paywallstester/Constants.kt index 93c21b87c6..59af1246ac 100644 --- a/examples/paywall-tester/src/main/java/com/revenuecat/paywallstester/Constants.kt +++ b/examples/paywall-tester/src/main/java/com/revenuecat/paywallstester/Constants.kt @@ -1,5 +1,5 @@ package com.revenuecat.paywallstester object Constants { - const val GOOGLE_API_KEY = "API_KEY" + const val GOOGLE_API_KEY = "goog_YcwEWqofjQvlwgbsJzlPIikpdaE" } diff --git a/examples/paywall-tester/src/main/java/com/revenuecat/paywallstester/SamplePaywalls.kt b/examples/paywall-tester/src/main/java/com/revenuecat/paywallstester/SamplePaywalls.kt index 9039d64af4..d4eabf3bfe 100644 --- a/examples/paywall-tester/src/main/java/com/revenuecat/paywallstester/SamplePaywalls.kt +++ b/examples/paywall-tester/src/main/java/com/revenuecat/paywallstester/SamplePaywalls.kt @@ -1,5 +1,10 @@ +@file:OptIn(InternalRevenueCatAPI::class) + package com.revenuecat.paywallstester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import com.revenuecat.purchases.InternalRevenueCatAPI import com.revenuecat.purchases.Offering import com.revenuecat.purchases.Package import com.revenuecat.purchases.PackageType @@ -8,6 +13,30 @@ import com.revenuecat.purchases.models.Price import com.revenuecat.purchases.models.TestStoreProduct import com.revenuecat.purchases.paywalls.PaywallColor import com.revenuecat.purchases.paywalls.PaywallData +import com.revenuecat.purchases.paywalls.components.StackComponent +import com.revenuecat.purchases.paywalls.components.TextComponent +import com.revenuecat.purchases.paywalls.components.common.Background +import com.revenuecat.purchases.paywalls.components.common.ComponentsConfig +import com.revenuecat.purchases.paywalls.components.common.LocaleId +import com.revenuecat.purchases.paywalls.components.common.LocalizationData +import com.revenuecat.purchases.paywalls.components.common.LocalizationKey +import com.revenuecat.purchases.paywalls.components.common.PaywallComponentsConfig +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.paywalls.components.properties.Dimension.Vertical +import com.revenuecat.purchases.paywalls.components.properties.Dimension.ZLayer +import com.revenuecat.purchases.paywalls.components.properties.FlexDistribution.END +import com.revenuecat.purchases.paywalls.components.properties.FontSize +import com.revenuecat.purchases.paywalls.components.properties.FontWeight +import com.revenuecat.purchases.paywalls.components.properties.HorizontalAlignment.LEADING +import com.revenuecat.purchases.paywalls.components.properties.Padding +import com.revenuecat.purchases.paywalls.components.properties.Shape +import com.revenuecat.purchases.paywalls.components.properties.Size +import com.revenuecat.purchases.paywalls.components.properties.SizeConstraint.Fill +import com.revenuecat.purchases.paywalls.components.properties.SizeConstraint.Fit +import com.revenuecat.purchases.paywalls.components.properties.TwoDimensionalAlignment +import com.revenuecat.purchases.paywalls.components.properties.TwoDimensionalAlignment.BOTTOM import java.net.URL class SamplePaywallsLoader { @@ -17,11 +46,12 @@ class SamplePaywallsLoader { SamplePaywalls.offeringIdentifier, emptyMap(), SamplePaywalls.packages, - paywall = paywallForTemplate(template), + paywall = (paywallForTemplate(template) as? SampleData.Legacy)?.data, + paywallComponents = (paywallForTemplate(template) as? SampleData.Components)?.data, ) } - private fun paywallForTemplate(template: SamplePaywalls.SampleTemplate): PaywallData { + private fun paywallForTemplate(template: SamplePaywalls.SampleTemplate): SampleData { return when (template) { SamplePaywalls.SampleTemplate.TEMPLATE_1 -> SamplePaywalls.template1() SamplePaywalls.SampleTemplate.TEMPLATE_2 -> SamplePaywalls.template2() @@ -29,11 +59,18 @@ class SamplePaywallsLoader { SamplePaywalls.SampleTemplate.TEMPLATE_4 -> SamplePaywalls.template4() SamplePaywalls.SampleTemplate.TEMPLATE_5 -> SamplePaywalls.template5() SamplePaywalls.SampleTemplate.TEMPLATE_7 -> SamplePaywalls.template7() + SamplePaywalls.SampleTemplate.COMPONENTS_BLESS -> SamplePaywalls.bless() SamplePaywalls.SampleTemplate.UNRECOGNIZED_TEMPLATE -> SamplePaywalls.unrecognizedTemplate() } } } +sealed interface SampleData { + data class Legacy(val data: PaywallData) : SampleData + + data class Components(val data: PaywallComponentsData) : SampleData +} + @SuppressWarnings("LongMethod", "LargeClass") object SamplePaywalls { @@ -44,6 +81,7 @@ object SamplePaywalls { TEMPLATE_4("#4: Horizontal packages"), TEMPLATE_5("#5: Minimalist with small banner"), TEMPLATE_7("#7: Multi-tier"), + COMPONENTS_BLESS("#8: Components - bless."), UNRECOGNIZED_TEMPLATE("Default template"), } @@ -186,8 +224,8 @@ object SamplePaywalls { lifetimePackage, ) - fun template1(): PaywallData { - return PaywallData( + fun template1(): SampleData.Legacy = SampleData.Legacy( + data = PaywallData( templateName = "1", config = PaywallData.Configuration( images = images, @@ -224,11 +262,11 @@ object SamplePaywalls { ), ), zeroDecimalPlaceCountries = zeroDecimalPlaceCountries, - ) - } + ), + ) - fun template2(): PaywallData { - return PaywallData( + fun template2(): SampleData.Legacy = SampleData.Legacy( + data = PaywallData( templateName = "2", config = PaywallData.Configuration( images = images, @@ -279,11 +317,11 @@ object SamplePaywalls { ), ), zeroDecimalPlaceCountries = zeroDecimalPlaceCountries, - ) - } + ), + ) - fun template3(): PaywallData { - return PaywallData( + fun template3(): SampleData.Legacy = SampleData.Legacy( + data = PaywallData( templateName = "3", config = PaywallData.Configuration( images = images, @@ -343,11 +381,11 @@ object SamplePaywalls { ), ), zeroDecimalPlaceCountries = zeroDecimalPlaceCountries, - ) - } + ), + ) - fun template4(): PaywallData { - return PaywallData( + fun template4(): SampleData.Legacy = SampleData.Legacy( + data = PaywallData( templateName = "4", config = PaywallData.Configuration( images = PaywallData.Configuration.Images( @@ -385,11 +423,11 @@ object SamplePaywalls { ), ), zeroDecimalPlaceCountries = zeroDecimalPlaceCountries, - ) - } + ), + ) - fun template5(): PaywallData { - return PaywallData( + fun template5(): SampleData.Legacy = SampleData.Legacy( + data = PaywallData( templateName = "5", config = PaywallData.Configuration( packageIds = listOf( @@ -448,11 +486,11 @@ object SamplePaywalls { ), ), zeroDecimalPlaceCountries = zeroDecimalPlaceCountries, - ) - } + ), + ) - fun template7(): PaywallData { - return PaywallData( + fun template7(): SampleData.Legacy = SampleData.Legacy( + data = PaywallData( templateName = "7", config = PaywallData.Configuration( packageIds = emptyList(), @@ -675,18 +713,180 @@ object SamplePaywalls { ), ), zeroDecimalPlaceCountries = zeroDecimalPlaceCountries, - ) - } + ), + ) - fun unrecognizedTemplate(): PaywallData { - return PaywallData( + fun unrecognizedTemplate(): SampleData.Legacy = SampleData.Legacy( + data = PaywallData( templateName = "unrecognized", - config = template4().config, + config = template4().data.config, assetBaseURL = paywallAssetBaseURL, localization = mapOf( - "en_US" to template4().localizedConfiguration.second, + "en_US" to template4().data.localizedConfiguration.second, ), zeroDecimalPlaceCountries = zeroDecimalPlaceCountries, + ), + ) + + /** + * [Inspiration](https://mobbin.com/screens/fd110266-4c8b-4673-9b51-48de70a4ae51) + */ + fun bless(): SampleData.Components { + val textColor = ColorScheme( + light = ColorInfo.Hex(Color.Black.toArgb()), + dark = ColorInfo.Hex(Color.White.toArgb()), + ) + val backgroundColor = ColorScheme( + light = ColorInfo.Hex(Color.White.toArgb()), + dark = ColorInfo.Hex(Color.Black.toArgb()), + ) + + return SampleData.Components( + data = PaywallComponentsData( + templateName = "template", + assetBaseURL = URL("https://assets.pawwalls.com"), + componentsConfig = ComponentsConfig( + base = PaywallComponentsConfig( + stack = StackComponent( + components = listOf( + StackComponent( + components = emptyList(), + dimension = ZLayer(alignment = TwoDimensionalAlignment.CENTER), + size = Size(width = Fill, height = Fill), + backgroundColor = ColorScheme( + light = ColorInfo.Gradient.Linear( + degrees = 60f, + points = listOf( + ColorInfo.Gradient.Point( + color = Color(red = 0xFF, green = 0xFF, blue = 0xFF, alpha = 0xFF) + .toArgb(), + percent = 0.4f, + ), + ColorInfo.Gradient.Point( + color = Color(red = 5, green = 124, blue = 91).toArgb(), + percent = 1f, + ), + ), + ), + ), + ), + StackComponent( + components = listOf( + TextComponent( + text = LocalizationKey("title"), + color = textColor, + fontWeight = FontWeight.SEMI_BOLD, + fontSize = FontSize.HEADING_L, + horizontalAlignment = LEADING, + size = Size(width = Fill, height = Fit), + margin = Padding(top = 0.0, bottom = 40.0, leading = 0.0, trailing = 0.0), + ), + TextComponent( + text = LocalizationKey("feature-1"), + color = textColor, + horizontalAlignment = LEADING, + size = Size(width = Fill, height = Fit), + margin = Padding(top = 8.0, bottom = 8.0, leading = 0.0, trailing = 0.0), + ), + TextComponent( + text = LocalizationKey("feature-2"), + color = textColor, + horizontalAlignment = LEADING, + size = Size(width = Fill, height = Fit), + margin = Padding(top = 8.0, bottom = 8.0, leading = 0.0, trailing = 0.0), + ), + TextComponent( + text = LocalizationKey("feature-3"), + color = textColor, + horizontalAlignment = LEADING, + size = Size(width = Fill, height = Fit), + margin = Padding(top = 8.0, bottom = 8.0, leading = 0.0, trailing = 0.0), + ), + TextComponent( + text = LocalizationKey("feature-4"), + color = textColor, + horizontalAlignment = LEADING, + size = Size(width = Fill, height = Fit), + margin = Padding(top = 8.0, bottom = 8.0, leading = 0.0, trailing = 0.0), + ), + TextComponent( + text = LocalizationKey("feature-5"), + color = textColor, + horizontalAlignment = LEADING, + size = Size(width = Fill, height = Fit), + margin = Padding(top = 8.0, bottom = 8.0, leading = 0.0, trailing = 0.0), + ), + TextComponent( + text = LocalizationKey("feature-6"), + color = textColor, + horizontalAlignment = LEADING, + size = Size(width = Fill, height = Fit), + margin = Padding(top = 8.0, bottom = 8.0, leading = 0.0, trailing = 0.0), + ), + TextComponent( + text = LocalizationKey("offer"), + color = textColor, + horizontalAlignment = LEADING, + size = Size(width = Fill, height = Fit), + margin = Padding(top = 48.0, bottom = 8.0, leading = 0.0, trailing = 0.0), + ), + StackComponent( + components = listOf( + TextComponent( + text = LocalizationKey("cta"), + color = ColorScheme( + light = ColorInfo.Hex(Color.White.toArgb()), + ), + fontWeight = FontWeight.BOLD, + ), + ), + dimension = ZLayer(alignment = TwoDimensionalAlignment.CENTER), + size = Size(width = Fit, height = Fit), + backgroundColor = ColorScheme( + light = ColorInfo.Hex(Color(red = 5, green = 124, blue = 91).toArgb()), + ), + padding = Padding(top = 8.0, bottom = 8.0, leading = 32.0, trailing = 32.0), + margin = Padding(top = 8.0, bottom = 8.0, leading = 0.0, trailing = 0.0), + shape = Shape.Pill, + ), + TextComponent( + text = LocalizationKey("terms"), + color = textColor, + ), + ), + dimension = Vertical(alignment = LEADING, distribution = END), + size = Size(width = Fill, height = Fill), + padding = Padding(top = 16.0, bottom = 16.0, leading = 32.0, trailing = 32.0), + ), + ), + dimension = ZLayer(alignment = BOTTOM), + size = Size(width = Fill, height = Fill), + backgroundColor = backgroundColor, + ), + background = Background.Color(backgroundColor), + stickyFooter = null, + ), + ), + componentsLocalizations = mapOf( + LocaleId("en_US") to mapOf( + LocalizationKey("title") to LocalizationData.Text("Unlock bless."), + LocalizationKey("feature-1") to LocalizationData.Text("✓ Enjoy a 7 day trial"), + LocalizationKey("feature-2") to LocalizationData.Text("✓ Change currencies"), + LocalizationKey("feature-3") to LocalizationData.Text("✓ Access more trend charts"), + LocalizationKey("feature-4") to LocalizationData.Text("✓ Create custom categories"), + LocalizationKey("feature-5") to LocalizationData.Text("✓ Get a special premium icon"), + LocalizationKey("feature-6") to LocalizationData.Text( + "✓ Receive our love and gratitude for your support", + ), + LocalizationKey("offer") to LocalizationData.Text( + "Try 7 days free, then $19.98/year. Cancel anytime.", + ), + LocalizationKey("cta") to LocalizationData.Text("Continue"), + LocalizationKey("terms") to LocalizationData.Text("Privacy & Terms"), + ), + ), + defaultLocaleIdentifier = LocaleId("en_US"), + ), ) } } diff --git a/examples/paywall-tester/src/main/java/com/revenuecat/paywallstester/ui/screens/main/offerings/OfferingsScreen.kt b/examples/paywall-tester/src/main/java/com/revenuecat/paywallstester/ui/screens/main/offerings/OfferingsScreen.kt index d6b05cb789..7157761673 100644 --- a/examples/paywall-tester/src/main/java/com/revenuecat/paywallstester/ui/screens/main/offerings/OfferingsScreen.kt +++ b/examples/paywall-tester/src/main/java/com/revenuecat/paywallstester/ui/screens/main/offerings/OfferingsScreen.kt @@ -33,6 +33,7 @@ import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.viewmodel.compose.viewModel import com.revenuecat.paywallstester.MainActivity +import com.revenuecat.purchases.InternalRevenueCatAPI import com.revenuecat.purchases.Offering import com.revenuecat.purchases.Offerings import com.revenuecat.purchases.Purchases @@ -84,6 +85,7 @@ private fun LoadingOfferingsScreen() { } } +@OptIn(InternalRevenueCatAPI::class) @Suppress("LongMethod") @Composable private fun OfferingsListScreen( @@ -138,11 +140,11 @@ private fun OfferingsListScreen( Column { Text(text = offering.identifier) - offering.paywall?.let { + offering.paywall?.also { Text("Template ${it.templateName}") - } ?: run { - Text("No paywall") - } + } ?: offering.paywallComponents?.also { + Text("Components ${it.templateName}") + } ?: Text("No paywall") } } Divider() diff --git a/examples/paywall-tester/src/main/java/com/revenuecat/paywallstester/ui/screens/main/paywalls/PaywallsScreen.kt b/examples/paywall-tester/src/main/java/com/revenuecat/paywallstester/ui/screens/main/paywalls/PaywallsScreen.kt index 732a73fb0e..f992435d8b 100644 --- a/examples/paywall-tester/src/main/java/com/revenuecat/paywallstester/ui/screens/main/paywalls/PaywallsScreen.kt +++ b/examples/paywall-tester/src/main/java/com/revenuecat/paywallstester/ui/screens/main/paywalls/PaywallsScreen.kt @@ -31,6 +31,7 @@ import com.revenuecat.paywallstester.SamplePaywallsLoader import com.revenuecat.paywallstester.ui.screens.paywallfooter.SamplePaywall import com.revenuecat.paywallstester.ui.theme.bundledLobsterTwoFontFamily import com.revenuecat.purchases.CustomerInfo +import com.revenuecat.purchases.InternalRevenueCatAPI import com.revenuecat.purchases.Offering import com.revenuecat.purchases.Package import com.revenuecat.purchases.PurchasesError @@ -94,6 +95,7 @@ private class TestAppPurchaseLogicCallbacks : PurchaseLogicWithCallback() { } } +@OptIn(InternalRevenueCatAPI::class) @Suppress("LongMethod") @Composable fun PaywallsScreen( @@ -132,39 +134,46 @@ fun PaywallsScreen( emoji = "\uD83D\uDCF1", label = "Full screen", ) - ButtonWithEmoji( - onClick = { - displayPaywallState = DisplayPaywallState.Footer( - offering, - condensed = false, - purchaseLogic = myAppPurchaseLogic, - ) - }, - emoji = "\uD83D\uDD3D", - label = "Footer", - ) - ButtonWithEmoji( - onClick = { - displayPaywallState = DisplayPaywallState.Footer( - offering, - condensed = true, - purchaseLogic = myAppPurchaseLogic, - ) - }, - emoji = "\uD83D\uDDDC️", - label = "Condenser footer", - ) - ButtonWithEmoji( - onClick = { - displayPaywallState = DisplayPaywallState.FullScreen( - offering, - CustomFontProvider(bundledLobsterTwoFontFamily), - purchaseLogic = myAppPurchaseLogic, - ) - }, - emoji = "\uD83C\uDD70️", - label = "Custom font", - ) + if (offering.paywallComponents == null) { + ButtonWithEmoji( + onClick = { + displayPaywallState = DisplayPaywallState.Footer( + offering, + condensed = false, + purchaseLogic = myAppPurchaseLogic, + ) + }, + emoji = "\uD83D\uDD3D", + label = "Footer", + ) + } + if (offering.paywallComponents == null) { + ButtonWithEmoji( + onClick = { + displayPaywallState = DisplayPaywallState.Footer( + offering, + condensed = true, + purchaseLogic = myAppPurchaseLogic, + ) + }, + emoji = "\uD83D\uDDDC️", + label = "Condenser footer", + ) + } + // Custom font is not yet supported by Paywalls V2. + if (offering.paywallComponents == null) { + ButtonWithEmoji( + onClick = { + displayPaywallState = DisplayPaywallState.FullScreen( + offering, + CustomFontProvider(bundledLobsterTwoFontFamily), + purchaseLogic = myAppPurchaseLogic, + ) + }, + emoji = "\uD83C\uDD70️", + label = "Custom font", + ) + } } } } From 8616a2604d4f6e7a7fbe2c87fede9b4a0a4aff22 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Thu, 12 Dec 2024 12:04:29 +0100 Subject: [PATCH 06/37] Adds missing circle MaskShape. --- .../paywalls/components/properties/MaskShape.kt | 4 ++++ .../paywalls/components/properties/MaskShapeTests.kt | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/components/properties/MaskShape.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/components/properties/MaskShape.kt index a71b91cc83..a17b973659 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/components/properties/MaskShape.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/components/properties/MaskShape.kt @@ -27,4 +27,8 @@ sealed interface MaskShape { @Serializable @SerialName("convex") object Convex : MaskShape + + @Serializable + @SerialName("circle") + object Circle : MaskShape } diff --git a/purchases/src/test/java/com/revenuecat/purchases/paywalls/components/properties/MaskShapeTests.kt b/purchases/src/test/java/com/revenuecat/purchases/paywalls/components/properties/MaskShapeTests.kt index 60219f8c08..0257213eaa 100644 --- a/purchases/src/test/java/com/revenuecat/purchases/paywalls/components/properties/MaskShapeTests.kt +++ b/purchases/src/test/java/com/revenuecat/purchases/paywalls/components/properties/MaskShapeTests.kt @@ -105,6 +105,18 @@ internal class MaskShapeTests(@Suppress("UNUSED_PARAMETER") name: String, privat expected = MaskShape.Convex ) ), + arrayOf( + "circle", + Args( + json = """ + { + "type": "circle", + "corners": null + } + """.trimIndent(), + expected = MaskShape.Circle + ) + ), ) } From bbe72ebf7ac459f06d444e8a1d70a586c9ee4c81 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Thu, 12 Dec 2024 12:05:16 +0100 Subject: [PATCH 07/37] LoadedPaywallComponents will always fill the maximum available size. --- .../ui/revenuecatui/components/LoadedPaywallComponents.kt | 1 + 1 file changed, 1 insertion(+) 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 773a1f8e39..88c3786181 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 @@ -87,6 +87,7 @@ internal fun LoadedPaywallComponents( ComponentView( style = style, modifier = modifier + .fillMaxSize() .background(background), ) } From 27837e2bfe998264f69d209873797c90345d8a14 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Thu, 12 Dec 2024 12:12:37 +0100 Subject: [PATCH 08/37] ImageUrls width and height are optional. --- .../components/properties/ImageUrls.kt | 4 +-- .../components/properties/ImageUrlsTests.kt | 25 +++++++++++++++++ .../components/style/ImageComponentStyle.kt | 28 +++++++++++-------- 3 files changed, 43 insertions(+), 14 deletions(-) diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/components/properties/ImageUrls.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/components/properties/ImageUrls.kt index 124102f924..00e06bb004 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/components/properties/ImageUrls.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/components/properties/ImageUrls.kt @@ -22,9 +22,9 @@ class ImageUrls( @Serializable(with = URLSerializer::class) val webpLowRes: URL, @get:JvmSynthetic - val width: UInt, + val width: UInt? = null, @get:JvmSynthetic - val height: UInt, + val height: UInt? = null, ) @InternalRevenueCatAPI diff --git a/purchases/src/test/java/com/revenuecat/purchases/paywalls/components/properties/ImageUrlsTests.kt b/purchases/src/test/java/com/revenuecat/purchases/paywalls/components/properties/ImageUrlsTests.kt index 360e7a5b1b..870f088fce 100644 --- a/purchases/src/test/java/com/revenuecat/purchases/paywalls/components/properties/ImageUrlsTests.kt +++ b/purchases/src/test/java/com/revenuecat/purchases/paywalls/components/properties/ImageUrlsTests.kt @@ -129,6 +129,31 @@ internal class ImageUrlsTests { ) ) ), + arrayOf( + "no width and height", + Args( + json = """ + { + "light": { + "heic": "https://assets.pawwalls.com/1151049_1732039548.heic", + "heic_low_res": "https://assets.pawwalls.com/1151049_low_res_1732039548.heic", + "original": "https://assets.pawwalls.com/1151049_1732039548.png", + "webp": "https://assets.pawwalls.com/1151049_1732039548.webp", + "webp_low_res": "https://assets.pawwalls.com/1151049_low_res_1732039548.webp" + } + } + """.trimIndent(), + expected = ThemeImageUrls( + light = ImageUrls( + original = URL("https://assets.pawwalls.com/1151049_1732039548.png"), + webp = URL("https://assets.pawwalls.com/1151049_1732039548.webp"), + webpLowRes = URL("https://assets.pawwalls.com/1151049_low_res_1732039548.webp"), + width = null, + height = null, + ), + ) + ) + ), ) } diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/style/ImageComponentStyle.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/style/ImageComponentStyle.kt index 0faa64d4df..d9e518a461 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/style/ImageComponentStyle.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/style/ImageComponentStyle.kt @@ -68,16 +68,20 @@ internal class ImageComponentStyle private constructor( private fun Size.adjustForImage(imageUrls: ImageUrls, density: Density): Size = Size( - width = when (width) { - is Fit -> Fixed(with(density) { imageUrls.width.toInt().toDp().value.toUInt() }) - is Fill, - is Fixed, - -> width - }, - height = when (height) { - is Fit -> Fixed(with(density) { imageUrls.height.toInt().toDp().value.toUInt() }) - is Fill, - is Fixed, - -> height - }, + width = imageUrls.width?.let { imageWidthPx -> + when (width) { + is Fit -> Fixed(with(density) { imageWidthPx.toInt().toDp().value.toUInt() }) + is Fill, + is Fixed, + -> width + } + } ?: width, + height = imageUrls.height?.let { imageHeightPx -> + when (height) { + is Fit -> Fixed(with(density) { imageHeightPx.toInt().toDp().value.toUInt() }) + is Fill, + is Fixed, + -> height + } + } ?: height, ) From da94c38235eec1fb2e370bc464d5bfb97c92b2dd Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Thu, 12 Dec 2024 13:27:59 +0100 Subject: [PATCH 09/37] Reverts Constants. --- .../src/main/java/com/revenuecat/paywallstester/Constants.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/paywall-tester/src/main/java/com/revenuecat/paywallstester/Constants.kt b/examples/paywall-tester/src/main/java/com/revenuecat/paywallstester/Constants.kt index 59af1246ac..93c21b87c6 100644 --- a/examples/paywall-tester/src/main/java/com/revenuecat/paywallstester/Constants.kt +++ b/examples/paywall-tester/src/main/java/com/revenuecat/paywallstester/Constants.kt @@ -1,5 +1,5 @@ package com.revenuecat.paywallstester object Constants { - const val GOOGLE_API_KEY = "goog_YcwEWqofjQvlwgbsJzlPIikpdaE" + const val GOOGLE_API_KEY = "API_KEY" } From b669421852906ca06a03e68ab7358d5117f4c06f Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Thu, 12 Dec 2024 13:44:45 +0100 Subject: [PATCH 10/37] Fixes lint. --- purchases/src/main/kotlin/com/revenuecat/purchases/Offering.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/Offering.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/Offering.kt index 108860ffd5..92f0832315 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/Offering.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/Offering.kt @@ -16,6 +16,7 @@ import com.revenuecat.purchases.paywalls.components.common.PaywallComponentsData * @property availablePackages Array of [Package] objects available for purchase. * @property metadata Offering metadata defined in RevenueCat dashboard. */ +@Suppress("UnsafeOptInUsageError") data class Offering @OptIn(InternalRevenueCatAPI::class) @JvmOverloads From 49e604ae5012d971dcad048d6c4849ccb5f5f1e5 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Thu, 12 Dec 2024 14:33:30 +0100 Subject: [PATCH 11/37] StackComponentView changes background color when the theme changes. --- .../components/button/ButtonComponentView.kt | 3 +- .../components/stack/StackComponentView.kt | 33 +++++-- .../components/style/StackComponentStyle.kt | 4 +- .../components/style/StyleFactory.kt | 3 +- .../stack/StackComponentViewTests.kt | 99 +++++++++++++++++++ 5 files changed, 128 insertions(+), 14 deletions(-) create mode 100644 ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/stack/StackComponentViewTests.kt diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/button/ButtonComponentView.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/button/ButtonComponentView.kt index 00ebf88863..21fbdd2dd3 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/button/ButtonComponentView.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/button/ButtonComponentView.kt @@ -22,7 +22,6 @@ import com.revenuecat.purchases.paywalls.components.properties.Padding import com.revenuecat.purchases.paywalls.components.properties.Size import com.revenuecat.purchases.paywalls.components.properties.SizeConstraint.Fit import com.revenuecat.purchases.ui.revenuecatui.components.PaywallAction -import com.revenuecat.purchases.ui.revenuecatui.components.properties.BackgroundStyle import com.revenuecat.purchases.ui.revenuecatui.components.properties.BorderStyle import com.revenuecat.purchases.ui.revenuecatui.components.properties.ColorStyle.Solid import com.revenuecat.purchases.ui.revenuecatui.components.properties.ShadowStyle @@ -75,7 +74,7 @@ private fun previewButtonComponentStyle( dimension = Dimension.Vertical(alignment = HorizontalAlignment.CENTER, distribution = START), size = Size(width = Fit, height = Fit), spacing = 16.dp, - background = BackgroundStyle.Color(Solid(Color.Red)), + backgroundColor = ColorScheme(light = ColorInfo.Hex(Color.Red.toArgb())), padding = PaddingValues(all = 16.dp), margin = PaddingValues(all = 16.dp), shape = RoundedCornerShape(size = 20.dp), diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/stack/StackComponentView.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/stack/StackComponentView.kt index ea53dfd0ce..a9b3006898 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/stack/StackComponentView.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/stack/StackComponentView.kt @@ -2,6 +2,7 @@ package com.revenuecat.purchases.ui.revenuecatui.components.stack +import android.content.res.Configuration import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -38,10 +39,10 @@ import com.revenuecat.purchases.ui.revenuecatui.components.modifier.background import com.revenuecat.purchases.ui.revenuecatui.components.modifier.border import com.revenuecat.purchases.ui.revenuecatui.components.modifier.shadow import com.revenuecat.purchases.ui.revenuecatui.components.modifier.size -import com.revenuecat.purchases.ui.revenuecatui.components.properties.BackgroundStyle import com.revenuecat.purchases.ui.revenuecatui.components.properties.BorderStyle import com.revenuecat.purchases.ui.revenuecatui.components.properties.ColorStyle.Solid import com.revenuecat.purchases.ui.revenuecatui.components.properties.ShadowStyle +import com.revenuecat.purchases.ui.revenuecatui.components.properties.rememberColorStyle import com.revenuecat.purchases.ui.revenuecatui.components.style.StackComponentStyle import com.revenuecat.purchases.ui.revenuecatui.components.style.TextComponentStyle import com.revenuecat.purchases.ui.revenuecatui.extensions.applyIfNotNull @@ -53,12 +54,14 @@ internal fun StackComponentView( modifier: Modifier = Modifier, ) { if (style.visible) { + val backgroundColorStyle = style.backgroundColor?.let { rememberColorStyle(scheme = it) } + // Modifier irrespective of dimension. val commonModifier = remember(style) { Modifier .padding(style.margin) .applyIfNotNull(style.shadow) { shadow(it, style.shape) } - .applyIfNotNull(style.background) { background(it, style.shape) } + .applyIfNotNull(backgroundColorStyle) { background(it, style.shape) } .clip(style.shape) .applyIfNotNull(style.border) { border(it, style.shape) } .padding(style.padding) @@ -104,7 +107,8 @@ internal fun StackComponentView( } } -@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES or Configuration.UI_MODE_TYPE_NORMAL) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO or Configuration.UI_MODE_TYPE_NORMAL) @Composable private fun StackComponentView_Preview_Vertical() { Box( @@ -117,7 +121,10 @@ private fun StackComponentView_Preview_Vertical() { dimension = Dimension.Vertical(alignment = HorizontalAlignment.CENTER, distribution = START), size = Size(width = Fit, height = Fit), spacing = 16.dp, - background = BackgroundStyle.Color(Solid(Color.Red)), + backgroundColor = ColorScheme( + light = ColorInfo.Hex(Color.Red.toArgb()), + dark = ColorInfo.Hex(Color.Yellow.toArgb()), + ), padding = PaddingValues(all = 16.dp), margin = PaddingValues(all = 16.dp), shape = RoundedCornerShape(size = 20.dp), @@ -128,7 +135,8 @@ private fun StackComponentView_Preview_Vertical() { } } -@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES or Configuration.UI_MODE_TYPE_NORMAL) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO or Configuration.UI_MODE_TYPE_NORMAL) @Composable private fun StackComponentView_Preview_Horizontal() { Box( @@ -141,7 +149,10 @@ private fun StackComponentView_Preview_Horizontal() { dimension = Dimension.Horizontal(alignment = VerticalAlignment.CENTER, distribution = START), size = Size(width = Fit, height = Fit), spacing = 16.dp, - background = BackgroundStyle.Color(Solid(Color.Red)), + backgroundColor = ColorScheme( + light = ColorInfo.Hex(Color.Red.toArgb()), + dark = ColorInfo.Hex(Color.Yellow.toArgb()), + ), padding = PaddingValues(all = 16.dp), margin = PaddingValues(all = 16.dp), shape = RoundedCornerShape(size = 20.dp), @@ -152,7 +163,9 @@ private fun StackComponentView_Preview_Horizontal() { } } -@Preview +@Suppress("LongMethod") +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES or Configuration.UI_MODE_TYPE_NORMAL) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO or Configuration.UI_MODE_TYPE_NORMAL) @Composable private fun StackComponentView_Preview_ZLayer() { Box( @@ -175,6 +188,7 @@ private fun StackComponentView_Preview_ZLayer() { horizontalAlignment = HorizontalAlignment.CENTER, backgroundColor = ColorScheme( light = ColorInfo.Hex(Color.Yellow.toArgb()), + dark = ColorInfo.Hex(Color.Red.toArgb()), ), size = Size(width = Fit, height = Fit), padding = Padding(top = 8.0, bottom = 8.0, leading = 8.0, trailing = 8.0), @@ -202,7 +216,10 @@ private fun StackComponentView_Preview_ZLayer() { dimension = Dimension.ZLayer(alignment = TwoDimensionalAlignment.BOTTOM_TRAILING), size = Size(width = Fit, height = Fit), spacing = 16.dp, - background = BackgroundStyle.Color(Solid(Color.Red)), + backgroundColor = ColorScheme( + light = ColorInfo.Hex(Color.Red.toArgb()), + dark = ColorInfo.Hex(Color.Yellow.toArgb()), + ), padding = PaddingValues(all = 16.dp), margin = PaddingValues(all = 16.dp), shape = RoundedCornerShape(size = 20.dp), diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/style/StackComponentStyle.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/style/StackComponentStyle.kt index 8676ce616b..dfdad06e30 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/style/StackComponentStyle.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/style/StackComponentStyle.kt @@ -4,9 +4,9 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Immutable import androidx.compose.ui.graphics.Shape import androidx.compose.ui.unit.Dp +import com.revenuecat.purchases.paywalls.components.properties.ColorScheme import com.revenuecat.purchases.paywalls.components.properties.Dimension import com.revenuecat.purchases.paywalls.components.properties.Size -import com.revenuecat.purchases.ui.revenuecatui.components.properties.BackgroundStyle import com.revenuecat.purchases.ui.revenuecatui.components.properties.BorderStyle import com.revenuecat.purchases.ui.revenuecatui.components.properties.ShadowStyle @@ -24,7 +24,7 @@ internal class StackComponentStyle( @get:JvmSynthetic val spacing: Dp, @get:JvmSynthetic - val background: BackgroundStyle?, + val backgroundColor: ColorScheme?, @get:JvmSynthetic val padding: PaddingValues, @get:JvmSynthetic 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 09ac577974..ef097e902e 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 @@ -23,7 +23,6 @@ import com.revenuecat.purchases.ui.revenuecatui.components.buildPresentedPartial import com.revenuecat.purchases.ui.revenuecatui.components.ktx.string import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toPaddingValues import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toShape -import com.revenuecat.purchases.ui.revenuecatui.components.properties.toBackgroundStyle import com.revenuecat.purchases.ui.revenuecatui.components.properties.toBorderStyle import com.revenuecat.purchases.ui.revenuecatui.components.properties.toShadowStyle import com.revenuecat.purchases.ui.revenuecatui.components.state.PackageContext @@ -96,7 +95,7 @@ internal class StyleFactory( dimension = partial?.dimension ?: component.dimension, size = partial?.size ?: component.size, spacing = (partial?.spacing ?: component.spacing ?: DEFAULT_SPACING).dp, - background = (partial?.backgroundColor ?: component.backgroundColor)?.toBackgroundStyle(), + backgroundColor = (partial?.backgroundColor ?: component.backgroundColor), padding = (partial?.padding ?: component.padding).toPaddingValues(), margin = (partial?.margin ?: component.margin).toPaddingValues(), shape = (partial?.shape ?: component.shape)?.toShape() ?: DEFAULT_SHAPE, 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 new file mode 100644 index 0000000000..5e7f3fd948 --- /dev/null +++ b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/stack/StackComponentViewTests.kt @@ -0,0 +1,99 @@ +package com.revenuecat.purchases.ui.revenuecatui.components.stack + +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.revenuecat.purchases.paywalls.components.StackComponent +import com.revenuecat.purchases.paywalls.components.properties.ColorInfo +import com.revenuecat.purchases.paywalls.components.properties.ColorScheme +import com.revenuecat.purchases.paywalls.components.properties.Size +import com.revenuecat.purchases.paywalls.components.properties.SizeConstraint +import com.revenuecat.purchases.ui.revenuecatui.assertions.assertPixelColorEquals +import com.revenuecat.purchases.ui.revenuecatui.components.ComponentViewState +import com.revenuecat.purchases.ui.revenuecatui.components.ScreenCondition +import com.revenuecat.purchases.ui.revenuecatui.components.state.PackageContext +import com.revenuecat.purchases.ui.revenuecatui.components.style.StackComponentStyle +import com.revenuecat.purchases.ui.revenuecatui.components.style.StyleFactory +import com.revenuecat.purchases.ui.revenuecatui.data.processed.VariableDataProvider +import com.revenuecat.purchases.ui.revenuecatui.data.testdata.MockResourceProvider +import com.revenuecat.purchases.ui.revenuecatui.helpers.getOrThrow +import com.revenuecat.purchases.ui.revenuecatui.helpers.themeChangingTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config +import org.robolectric.annotation.GraphicsMode +import org.robolectric.shadows.ShadowPixelCopy +import java.util.Locale + +@RunWith(AndroidJUnit4::class) +class StackComponentViewTests { + + @get:Rule + val composeTestRule = createComposeRule() + + private lateinit var styleFactory: StyleFactory + + @Before + fun setup() { + styleFactory = StyleFactory( + windowSize = ScreenCondition.COMPACT, + isEligibleForIntroOffer = true, + componentState = ComponentViewState.DEFAULT, + packageContext = PackageContext( + initialSelectedPackage = null, + initialVariableContext = PackageContext.VariableContext( + packages = emptyList(), + showZeroDecimalPlacePrices = false + ) + ), + localizationDictionary = emptyMap(), + locale = Locale.US, + variables = VariableDataProvider(MockResourceProvider()) + ) + } + + @GraphicsMode(GraphicsMode.Mode.NATIVE) + @Config(shadows = [ShadowPixelCopy::class], sdk = [26]) + @Test + fun `Should change background color based on theme`(): Unit = with(composeTestRule) { + // Arrange + val expectedLightColor = Color.Red + val expectedDarkColor = Color.Yellow + val component = StackComponent( + components = emptyList(), + size = Size(SizeConstraint.Fixed(100u), SizeConstraint.Fixed(100u)), + backgroundColor = ColorScheme( + light = ColorInfo.Hex(expectedLightColor.toArgb()), + dark = ColorInfo.Hex(expectedDarkColor.toArgb()), + ), + ) + + themeChangingTest( + arrange = { + // We don't want to recreate the entire tree every time the theme, or any other state, changes. + styleFactory.create(component).getOrThrow() as StackComponentStyle + }, + act = { StackComponentView(style = it, modifier = Modifier.testTag("stack")) }, + assert = { theme -> + theme.setLight() + onNodeWithTag("stack") + .assertIsDisplayed() + .assertPixelColorEquals(startX = 0, startY = 0, width = 4, height = 4, color = expectedLightColor) + + theme.setDark() + onNodeWithTag("stack") + .assertIsDisplayed() + .assertPixelColorEquals(startX = 0, startY = 0, width = 4, height = 4, color = expectedDarkColor) + } + ) + } + + +} From 2b5574d43384ee2b7b28f6c81d4ea649b9370919 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Thu, 12 Dec 2024 17:10:07 +0100 Subject: [PATCH 12/37] Adds a failing test to StackComponentViewTests. --- .../paywalls/components/properties/Border.kt | 2 +- .../stack/StackComponentViewTests.kt | 123 +++++++++++++++++- 2 files changed, 122 insertions(+), 3 deletions(-) diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/components/properties/Border.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/components/properties/Border.kt index e0f30daaf6..3057b887ba 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/components/properties/Border.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/components/properties/Border.kt @@ -7,7 +7,7 @@ import kotlinx.serialization.Serializable @InternalRevenueCatAPI @Poko @Serializable -class Border internal constructor( +class Border( @get:JvmSynthetic val color: ColorScheme, @get:JvmSynthetic 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 5e7f3fd948..c7d3b5a315 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 @@ -3,16 +3,20 @@ package com.revenuecat.purchases.ui.revenuecatui.components.stack import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.testTag +import androidx.compose.ui.test.SemanticsNodeInteraction import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 import com.revenuecat.purchases.paywalls.components.StackComponent +import com.revenuecat.purchases.paywalls.components.properties.Border import com.revenuecat.purchases.paywalls.components.properties.ColorInfo import com.revenuecat.purchases.paywalls.components.properties.ColorScheme import com.revenuecat.purchases.paywalls.components.properties.Size -import com.revenuecat.purchases.paywalls.components.properties.SizeConstraint +import com.revenuecat.purchases.paywalls.components.properties.SizeConstraint.Fixed import com.revenuecat.purchases.ui.revenuecatui.assertions.assertPixelColorEquals import com.revenuecat.purchases.ui.revenuecatui.components.ComponentViewState import com.revenuecat.purchases.ui.revenuecatui.components.ScreenCondition @@ -68,7 +72,7 @@ class StackComponentViewTests { val expectedDarkColor = Color.Yellow val component = StackComponent( components = emptyList(), - size = Size(SizeConstraint.Fixed(100u), SizeConstraint.Fixed(100u)), + size = Size(Fixed(100u), Fixed(100u)), backgroundColor = ColorScheme( light = ColorInfo.Hex(expectedLightColor.toArgb()), dark = ColorInfo.Hex(expectedDarkColor.toArgb()), @@ -95,5 +99,120 @@ class StackComponentViewTests { ) } + @GraphicsMode(GraphicsMode.Mode.NATIVE) + @Config(shadows = [ShadowPixelCopy::class], sdk = [26]) + @Test + fun `Should change border color based on theme`(): Unit = with(composeTestRule) { + // Arrange + val sizeDp = 100 + val borderWidthDp = 10.0 + val expectedLightColor = Color.Red + val expectedDarkColor = Color.Yellow + val expectedBackgroundColor = Color.White + val component = StackComponent( + components = emptyList(), + size = Size(Fixed(sizeDp.toUInt()), Fixed(sizeDp.toUInt())), + backgroundColor = ColorScheme(light = ColorInfo.Hex(expectedBackgroundColor.toArgb())), + border = Border( + color = ColorScheme( + light = ColorInfo.Hex(expectedLightColor.toArgb()), + dark = ColorInfo.Hex(expectedDarkColor.toArgb()), + ), + width = borderWidthDp + ), + ) + var borderWidthPx: Int? = null + var sizePx: Int? = null + + themeChangingTest( + arrange = { + // Use the Composable context to calculate px equivalents of our dp values. + borderWidthPx = with(LocalDensity.current) { borderWidthDp.dp.roundToPx() } + sizePx = with(LocalDensity.current) { sizeDp.dp.roundToPx() } + // We don't want to recreate the entire tree every time the theme, or any other state, changes. + styleFactory.create(component).getOrThrow() as StackComponentStyle + }, + act = { StackComponentView(style = it, modifier = Modifier.testTag("stack")) }, + assert = { theme -> + theme.setLight() + onNodeWithTag("stack") + .assertIsDisplayed() + .assertSquareBorderColor( + sizePx = sizePx!!, + borderWidthPx = borderWidthPx!!, + expectedBorderColor = expectedLightColor, + expectedBackgroundColor = expectedBackgroundColor, + ) + theme.setDark() + onNodeWithTag("stack") + .assertIsDisplayed() + .assertSquareBorderColor( + sizePx = sizePx!!, + borderWidthPx = borderWidthPx!!, + expectedBorderColor = expectedDarkColor, + expectedBackgroundColor = expectedBackgroundColor, + ) + } + ) + } + + @GraphicsMode(GraphicsMode.Mode.NATIVE) + @Config(shadows = [ShadowPixelCopy::class], sdk = [26]) + @Test + fun `Should change shadow color based on theme`(): Unit = with(composeTestRule) { + // TODO + } + + /** + * Asserts the border color of a square element. + * + * @param sizePx The size (height & width) of the square element, in px. + */ + private fun SemanticsNodeInteraction.assertSquareBorderColor( + sizePx: Int, + borderWidthPx: Int, + expectedBorderColor: Color, + expectedBackgroundColor: Color, + ): SemanticsNodeInteraction = + // Top edge + assertPixelColorEquals( + startX = 0, + startY = 0, + width = sizePx, + height = borderWidthPx, + color = expectedBorderColor + ) + // Left edge + .assertPixelColorEquals( + startX = 0, + startY = 0, + width = borderWidthPx, + height = sizePx, + color = expectedBorderColor + ) + // Right edge + .assertPixelColorEquals( + startX = sizePx - borderWidthPx, + startY = 0, + width = borderWidthPx, + height = sizePx, + color = expectedBorderColor + ) + // Bottom edge + .assertPixelColorEquals( + startX = 0, + startY = sizePx - borderWidthPx, + width = sizePx, + height = borderWidthPx, + color = expectedBorderColor + ) + // Inner area + .assertPixelColorEquals( + startX = borderWidthPx, + startY = borderWidthPx, + width = sizePx - borderWidthPx - borderWidthPx, + height = sizePx - borderWidthPx - borderWidthPx, + color = expectedBackgroundColor + ) } From 727150650f31e172f2e0ef5359746dd8d961be7f Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Thu, 12 Dec 2024 17:12:37 +0100 Subject: [PATCH 13/37] Adds 2 previews. --- .../components/LoadedPaywallComponents.kt | 184 ++++++++++++++++++ .../components/text/TextComponentView.kt | 13 ++ 2 files changed, 197 insertions(+) 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 88c3786181..5c7bae5290 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 @@ -30,10 +30,21 @@ import com.revenuecat.purchases.paywalls.components.properties.ColorInfo import com.revenuecat.purchases.paywalls.components.properties.ColorScheme import com.revenuecat.purchases.paywalls.components.properties.CornerRadiuses import com.revenuecat.purchases.paywalls.components.properties.Dimension.Vertical +import com.revenuecat.purchases.paywalls.components.properties.Dimension.ZLayer +import com.revenuecat.purchases.paywalls.components.properties.FlexDistribution.END import com.revenuecat.purchases.paywalls.components.properties.FlexDistribution.START +import com.revenuecat.purchases.paywalls.components.properties.FontSize +import com.revenuecat.purchases.paywalls.components.properties.FontWeight import com.revenuecat.purchases.paywalls.components.properties.HorizontalAlignment.CENTER +import com.revenuecat.purchases.paywalls.components.properties.HorizontalAlignment.LEADING +import com.revenuecat.purchases.paywalls.components.properties.Padding import com.revenuecat.purchases.paywalls.components.properties.Shadow import com.revenuecat.purchases.paywalls.components.properties.Shape +import com.revenuecat.purchases.paywalls.components.properties.Size +import com.revenuecat.purchases.paywalls.components.properties.SizeConstraint.Fill +import com.revenuecat.purchases.paywalls.components.properties.SizeConstraint.Fit +import com.revenuecat.purchases.paywalls.components.properties.TwoDimensionalAlignment +import com.revenuecat.purchases.paywalls.components.properties.TwoDimensionalAlignment.BOTTOM import com.revenuecat.purchases.ui.revenuecatui.components.modifier.background import com.revenuecat.purchases.ui.revenuecatui.components.properties.toBackgroundStyle import com.revenuecat.purchases.ui.revenuecatui.components.state.PackageContext @@ -185,3 +196,176 @@ private fun LoadedPaywallComponents_Preview() { .fillMaxSize(), ) } + +@Suppress("LongMethod") +@Preview() +@Composable +private fun LoadedPaywallComponents_Preview_Bless() { + val textColor = ColorScheme( + light = ColorInfo.Hex(Color.Black.toArgb()), + dark = ColorInfo.Hex(Color.White.toArgb()), + ) + val backgroundColor = ColorScheme( + light = ColorInfo.Hex(Color.White.toArgb()), + dark = ColorInfo.Hex(Color.Black.toArgb()), + ) + + LoadedPaywallComponents( + state = PaywallState.Loaded.Components( + offering = Offering( + identifier = "id", + serverDescription = "description", + metadata = emptyMap(), + availablePackages = emptyList(), + paywall = null, + ), + data = PaywallComponentsData( + templateName = "template", + assetBaseURL = URL("https://assets.pawwalls.com"), + componentsConfig = ComponentsConfig( + base = PaywallComponentsConfig( + stack = StackComponent( + components = listOf( + StackComponent( + components = emptyList(), + dimension = ZLayer(alignment = TwoDimensionalAlignment.CENTER), + size = Size(width = Fill, height = Fill), + backgroundColor = ColorScheme( + light = ColorInfo.Gradient.Linear( + degrees = 60f, + points = listOf( + ColorInfo.Gradient.Point( + color = Color(red = 0xFF, green = 0xFF, blue = 0xFF, alpha = 0xFF) + .toArgb(), + percent = 0.4f, + ), + ColorInfo.Gradient.Point( + color = Color(red = 5, green = 124, blue = 91).toArgb(), + percent = 1f, + ), + ), + ), + ), + ), + StackComponent( + components = listOf( + TextComponent( + text = LocalizationKey("title"), + color = textColor, + fontWeight = FontWeight.SEMI_BOLD, + fontSize = FontSize.HEADING_L, + horizontalAlignment = LEADING, + size = Size(width = Fill, height = Fit), + margin = Padding(top = 0.0, bottom = 40.0, leading = 0.0, trailing = 0.0), + ), + TextComponent( + text = LocalizationKey("feature-1"), + color = textColor, + horizontalAlignment = LEADING, + size = Size(width = Fill, height = Fit), + margin = Padding(top = 8.0, bottom = 8.0, leading = 0.0, trailing = 0.0), + ), + TextComponent( + text = LocalizationKey("feature-2"), + color = textColor, + horizontalAlignment = LEADING, + size = Size(width = Fill, height = Fit), + margin = Padding(top = 8.0, bottom = 8.0, leading = 0.0, trailing = 0.0), + ), + TextComponent( + text = LocalizationKey("feature-3"), + color = textColor, + horizontalAlignment = LEADING, + size = Size(width = Fill, height = Fit), + margin = Padding(top = 8.0, bottom = 8.0, leading = 0.0, trailing = 0.0), + ), + TextComponent( + text = LocalizationKey("feature-4"), + color = textColor, + horizontalAlignment = LEADING, + size = Size(width = Fill, height = Fit), + margin = Padding(top = 8.0, bottom = 8.0, leading = 0.0, trailing = 0.0), + ), + TextComponent( + text = LocalizationKey("feature-5"), + color = textColor, + horizontalAlignment = LEADING, + size = Size(width = Fill, height = Fit), + margin = Padding(top = 8.0, bottom = 8.0, leading = 0.0, trailing = 0.0), + ), + TextComponent( + text = LocalizationKey("feature-6"), + color = textColor, + horizontalAlignment = LEADING, + size = Size(width = Fill, height = Fit), + margin = Padding(top = 8.0, bottom = 8.0, leading = 0.0, trailing = 0.0), + ), + TextComponent( + text = LocalizationKey("offer"), + color = textColor, + horizontalAlignment = LEADING, + size = Size(width = Fill, height = Fit), + margin = Padding(top = 48.0, bottom = 8.0, leading = 0.0, trailing = 0.0), + ), + StackComponent( + components = listOf( + TextComponent( + text = LocalizationKey("cta"), + color = ColorScheme( + light = ColorInfo.Hex(Color.White.toArgb()), + ), + fontWeight = FontWeight.BOLD, + ), + ), + dimension = ZLayer(alignment = TwoDimensionalAlignment.CENTER), + size = Size(width = Fit, height = Fit), + backgroundColor = ColorScheme( + light = ColorInfo.Hex(Color(red = 5, green = 124, blue = 91).toArgb()), + ), + padding = Padding(top = 8.0, bottom = 8.0, leading = 32.0, trailing = 32.0), + margin = Padding(top = 8.0, bottom = 8.0, leading = 0.0, trailing = 0.0), + shape = Shape.Pill, + ), + TextComponent( + text = LocalizationKey("terms"), + color = textColor, + ), + ), + dimension = Vertical(alignment = LEADING, distribution = END), + size = Size(width = Fill, height = Fill), + padding = Padding(top = 16.0, bottom = 16.0, leading = 32.0, trailing = 32.0), + ), + ), + dimension = ZLayer(alignment = BOTTOM), + size = Size(width = Fill, height = Fill), + backgroundColor = backgroundColor, + ), + background = Background.Color(backgroundColor), + stickyFooter = null, + ), + ), + componentsLocalizations = mapOf( + LocaleId("en_US") to mapOf( + LocalizationKey("title") to LocalizationData.Text("Unlock bless."), + LocalizationKey("feature-1") to LocalizationData.Text("✓ Enjoy a 7 day trial"), + LocalizationKey("feature-2") to LocalizationData.Text("✓ Change currencies"), + LocalizationKey("feature-3") to LocalizationData.Text("✓ Access more trend charts"), + LocalizationKey("feature-4") to LocalizationData.Text("✓ Create custom categories"), + LocalizationKey("feature-5") to LocalizationData.Text("✓ Get a special premium icon"), + LocalizationKey("feature-6") to LocalizationData.Text( + "✓ Receive our love and gratitude for your support", + ), + LocalizationKey("offer") to LocalizationData.Text( + "Try 7 days free, then $19.98/year. Cancel anytime.", + ), + LocalizationKey("cta") to LocalizationData.Text("Continue"), + LocalizationKey("terms") to LocalizationData.Text("Privacy & Terms"), + ), + ), + defaultLocaleIdentifier = LocaleId("en_US"), + ), + ), + modifier = Modifier + .fillMaxSize(), + ) +} diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/text/TextComponentView.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/text/TextComponentView.kt index 882d0dea23..7d2c5fd44b 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/text/TextComponentView.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/text/TextComponentView.kt @@ -134,6 +134,19 @@ private fun TextComponentView_Preview_CursiveFont() { ) } +@Preview +@Composable +private fun TextComponentView_Preview_FontSize() { + TextComponentView( + style = previewTextComponentStyle( + text = "Hello, world", + color = ColorScheme(light = ColorInfo.Hex(Color.Black.toArgb())), + fontSize = FontSize.HEADING_L, + size = Size(width = Fit, height = Fit), + ), + ) +} + @Preview(name = "HorizontalAlignment") @Composable private fun TextComponentView_Preview_HorizontalAlignment() { From dc5810196e31076fffeef3100c50953e00f25dee Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Thu, 12 Dec 2024 17:13:58 +0100 Subject: [PATCH 14/37] Ensures MDParagraph uses the correct fontSize. --- .../purchases/ui/revenuecatui/composables/Markdown.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/composables/Markdown.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/composables/Markdown.kt index 2f7733bc88..74291376a0 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/composables/Markdown.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/composables/Markdown.kt @@ -202,7 +202,7 @@ private fun MDParagraph( val styledText = buildAnnotatedString { pushStyle( style - .copy(fontWeight = fontWeight) + .copy(fontWeight = fontWeight, fontSize = fontSize) .toSpanStyle(), ) appendMarkdownChildren(paragraph as Node, color, allowLinks) From 7bc2786fa15784370ca36c8825432802c85fecb9 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Thu, 12 Dec 2024 18:37:08 +0100 Subject: [PATCH 15/37] Adds another failing test to StackComponentViewTests. --- .../assertions/assertPixelColorEquals.kt | 128 +++++++++++++----- .../stack/StackComponentViewTests.kt | 79 +++++++++-- 2 files changed, 163 insertions(+), 44 deletions(-) diff --git a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/assertions/assertPixelColorEquals.kt b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/assertions/assertPixelColorEquals.kt index 248fad5e12..30ddb3ca20 100644 --- a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/assertions/assertPixelColorEquals.kt +++ b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/assertions/assertPixelColorEquals.kt @@ -17,25 +17,27 @@ import com.revenuecat.purchases.ui.revenuecatui.helpers.captureToImageCompat * @Config(shadows = [ShadowPixelCopy::class], sdk = [26]) * ``` * - * @param startX The x-coordinate of the first pixel to read from the Composable. - * @param startY The y-coordinate of the first pixel to read from the Composable. - * @param width The number of pixels to read from each row. - * @param height The number of rows to read. * @param color The color to assert. + * @param startX The x-coordinate of the first pixel to read from the Composable. Defaults to 0. + * @param startY The y-coordinate of the first pixel to read from the Composable. Defaults to 0. + * @param width The number of pixels to read from each row. Will read the entire row if this parameter is null. + * @param height The number of rows to read. Will read all rows if this parameter is null. */ internal fun SemanticsNodeInteraction.assertPixelColorEquals( - startX: Int, - startY: Int, - width: Int, - height: Int, color: Color, + startX: Int = 0, + startY: Int = 0, + width: Int? = null, + height: Int? = null, ): SemanticsNodeInteraction { val colorArgbInt = color.toArgb() + + val (widthToUse, heightToUse) = getDimensionsIfNull(width = width, height = height) val pixels = readPixels( startX = startX, startY = startY, - width = width, - height = height, + width = widthToUse, + height = heightToUse, ) return assert( @@ -51,6 +53,31 @@ internal fun SemanticsNodeInteraction.assertPixelColorEquals( ) } +/** + * Assert that the pixels in a rectangular area of this Composable do not have the provided [color]. + * + * When running on the JVM, make sure your test class or function has the following annotations. `sdk` has to be >= 26. + * + * ```kotlin + * @GraphicsMode(GraphicsMode.Mode.NATIVE) + * @Config(shadows = [ShadowPixelCopy::class], sdk = [26]) + * ``` + * + * @param color The color to assert. + * @param startX The x-coordinate of the first pixel to read from the Composable. Defaults to 0. + * @param startY The y-coordinate of the first pixel to read from the Composable. Defaults to 0. + * @param width The number of pixels to read from each row. Will read the entire row if this parameter is null. + * @param height The number of rows to read. Will read all rows if this parameter is null. + */ +internal fun SemanticsNodeInteraction.assertNoPixelColorEquals( + color: Color, + startX: Int = 0, + startY: Int = 0, + width: Int? = null, + height: Int? = null, +): SemanticsNodeInteraction = + assertPixelColorCount(color = color, startX = startX, startY = startY, width = width, height = height) { it == 0} + /** * Assert the number of pixels in a rectangular area of this Composable that have the provided [color]. * @@ -61,27 +88,28 @@ internal fun SemanticsNodeInteraction.assertPixelColorEquals( * @Config(shadows = [ShadowPixelCopy::class], sdk = [26]) * ``` * - * @param startX The x-coordinate of the first pixel to read from the Composable. - * @param startY The y-coordinate of the first pixel to read from the Composable. - * @param width The number of pixels to read from each row. - * @param height The number of rows to read. * @param color The color to assert. + * @param startX The x-coordinate of the first pixel to read from the Composable. Defaults to 0. + * @param startY The y-coordinate of the first pixel to read from the Composable. Defaults to 0. + * @param width The number of pixels to read from each row. Will read the entire row if this parameter is null. + * @param height The number of rows to read. Will read all rows if this parameter is null. * @param predicate The assertion you want to run. */ @Suppress("LongParameterList") internal fun SemanticsNodeInteraction.assertPixelColorCount( - startX: Int, - startY: Int, - width: Int, - height: Int, color: Color, + startX: Int = 0, + startY: Int = 0, + width: Int? = null, + height: Int? = null, predicate: (count: Int) -> Boolean, ): SemanticsNodeInteraction { + val (widthToUse, heightToUse) = getDimensionsIfNull(width = width, height = height) val pixels = readPixels( startX = startX, startY = startY, - width = width, - height = height, + width = widthToUse, + height = heightToUse, ) return assert( @@ -106,33 +134,43 @@ internal fun SemanticsNodeInteraction.assertPixelColorCount( * @Config(shadows = [ShadowPixelCopy::class], sdk = [26]) * ``` * - * @param startX The x-coordinate of the first pixel to read from the Composable. - * @param startY The y-coordinate of the first pixel to read from the Composable. - * @param width The number of pixels to read from each row. - * @param height The number of rows to read. * @param color The color to assert. + * @param startX The x-coordinate of the first pixel to read from the Composable. Defaults to 0. + * @param startY The y-coordinate of the first pixel to read from the Composable. Defaults to 0. + * @param width The number of pixels to read from each row. Will read the entire row if this parameter is null. + * @param height The number of rows to read. Will read all rows if this parameter is null. * @param predicate The assertion you want to run. The `percentage` parameter is in range 0..100. */ @Suppress("LongParameterList") internal fun SemanticsNodeInteraction.assertPixelColorPercentage( - startX: Int, - startY: Int, - width: Int, - height: Int, color: Color, + startX: Int = 0, + startY: Int = 0, + width: Int? = null, + height: Int? = null, predicate: (percentage: Float) -> Boolean, -): SemanticsNodeInteraction = - assertPixelColorCount( +): SemanticsNodeInteraction { + val (widthToUse, heightToUse) = getDimensionsIfNull(width = width, height = height) + val pixels = readPixels( startX = startX, startY = startY, - width = width, - height = height, - color = color, - predicate = { count -> - val percentage = count.toFloat() / (width * height) + width = widthToUse, + height = heightToUse, + ) + + return assert( + SemanticsMatcher("Assert count of pixels with color '$color'") { + val count = pixels + .groupBy { color -> color } + .mapValues { (_, pixels) -> pixels.count() } + .getOrDefault(color.toArgb(), defaultValue = 0) + + val percentage = count.toFloat() / (widthToUse * heightToUse) + predicate(percentage) } ) +} /** @@ -161,3 +199,23 @@ private fun SemanticsNodeInteraction.readPixels( return pixels } + +/** + * Gets the SemanticsNode's width if the provided [width] is null, and the SemanticsNode's height if the provided + * [height] is null. + * + * @return A Pair containing `width to height`. + */ +private fun SemanticsNodeInteraction.getDimensionsIfNull(width: Int?, height: Int?): Pair { + val widthToUse: Int + val heightToUse: Int + if (width == null || height == null) { + val size = fetchSemanticsNode().size + widthToUse = width ?: size.width + heightToUse = height ?: size.height + } else { + widthToUse = width + heightToUse = height + } + return widthToUse to heightToUse +} 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 c7d3b5a315..176fdd9f99 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 @@ -1,5 +1,9 @@ package com.revenuecat.purchases.ui.revenuecatui.components.stack +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.requiredSize +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb @@ -15,9 +19,12 @@ import com.revenuecat.purchases.paywalls.components.StackComponent import com.revenuecat.purchases.paywalls.components.properties.Border import com.revenuecat.purchases.paywalls.components.properties.ColorInfo import com.revenuecat.purchases.paywalls.components.properties.ColorScheme +import com.revenuecat.purchases.paywalls.components.properties.Shadow import com.revenuecat.purchases.paywalls.components.properties.Size import com.revenuecat.purchases.paywalls.components.properties.SizeConstraint.Fixed +import com.revenuecat.purchases.ui.revenuecatui.assertions.assertNoPixelColorEquals import com.revenuecat.purchases.ui.revenuecatui.assertions.assertPixelColorEquals +import com.revenuecat.purchases.ui.revenuecatui.assertions.assertPixelColorPercentage import com.revenuecat.purchases.ui.revenuecatui.components.ComponentViewState import com.revenuecat.purchases.ui.revenuecatui.components.ScreenCondition import com.revenuecat.purchases.ui.revenuecatui.components.state.PackageContext @@ -36,6 +43,8 @@ import org.robolectric.annotation.GraphicsMode import org.robolectric.shadows.ShadowPixelCopy import java.util.Locale +@GraphicsMode(GraphicsMode.Mode.NATIVE) +@Config(shadows = [ShadowPixelCopy::class], sdk = [26]) @RunWith(AndroidJUnit4::class) class StackComponentViewTests { @@ -63,8 +72,6 @@ class StackComponentViewTests { ) } - @GraphicsMode(GraphicsMode.Mode.NATIVE) - @Config(shadows = [ShadowPixelCopy::class], sdk = [26]) @Test fun `Should change background color based on theme`(): Unit = with(composeTestRule) { // Arrange @@ -99,13 +106,13 @@ class StackComponentViewTests { ) } - @GraphicsMode(GraphicsMode.Mode.NATIVE) - @Config(shadows = [ShadowPixelCopy::class], sdk = [26]) @Test fun `Should change border color based on theme`(): Unit = with(composeTestRule) { // Arrange val sizeDp = 100 val borderWidthDp = 10.0 + var sizePx: Int? = null + var borderWidthPx: Int? = null val expectedLightColor = Color.Red val expectedDarkColor = Color.Yellow val expectedBackgroundColor = Color.White @@ -121,8 +128,6 @@ class StackComponentViewTests { width = borderWidthDp ), ) - var borderWidthPx: Int? = null - var sizePx: Int? = null themeChangingTest( arrange = { @@ -157,11 +162,67 @@ class StackComponentViewTests { ) } - @GraphicsMode(GraphicsMode.Mode.NATIVE) - @Config(shadows = [ShadowPixelCopy::class], sdk = [26]) @Test fun `Should change shadow color based on theme`(): Unit = with(composeTestRule) { - // TODO + // Arrange + val parentSizeDp = 200 + val stackSizeDp = 100 + val expectedLightColor = Color.Red + val expectedDarkColor = Color.Yellow + val expectedBackgroundColor = Color.White + val component = StackComponent( + components = emptyList(), + size = Size(Fixed(stackSizeDp.toUInt()), Fixed(stackSizeDp.toUInt())), + shadow = Shadow( + color = ColorScheme( + light = ColorInfo.Hex(expectedLightColor.toArgb()), + dark = ColorInfo.Hex(expectedDarkColor.toArgb()), + ), + radius = 5.0, + x = 10.0, + y = 10.0, + ), + backgroundColor = ColorScheme(light = ColorInfo.Hex(expectedBackgroundColor.toArgb())), + ) + + themeChangingTest( + arrange = { + // We don't want to recreate the entire tree every time the theme, or any other state, changes. + styleFactory.create(component).getOrThrow() as StackComponentStyle + }, + act = { + // An outer box, because a shadow draws outside the Composable's bounds. + Box( + modifier = Modifier + .testTag(tag = "parent") + .requiredSize(parentSizeDp.dp) + .background(expectedBackgroundColor), + contentAlignment = Alignment.Center, + ) { + StackComponentView(style = it, modifier = Modifier.testTag("stack")) + } + }, + assert = { theme -> + onNodeWithTag("stack") + .assertIsDisplayed() + // No inner shadow, so the entire stack should be the same color. + .assertPixelColorEquals(expectedBackgroundColor) + + theme.setLight() + onNodeWithTag("parent") + .assertIsDisplayed() + // When the shadow is drawn, at least some pixels are the exact color we're looking for. + .assertPixelColorPercentage(expectedLightColor) { it > 0f } + .assertNoPixelColorEquals(expectedDarkColor) + + theme.setDark() + onNodeWithTag("parent") + .assertIsDisplayed() + // When the shadow is drawn, at least some pixels are the exact color we're looking for. + .assertPixelColorPercentage(expectedDarkColor) { it > 0f } + .assertNoPixelColorEquals(expectedLightColor) + } + ) } /** From 38401ed09e3de44c380a2d13d568fb12017903de Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Thu, 12 Dec 2024 18:38:44 +0100 Subject: [PATCH 16/37] Removing some redundant parameters. --- .../revenuecatui/components/stack/StackComponentViewTests.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 176fdd9f99..04b1017937 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 @@ -96,12 +96,12 @@ class StackComponentViewTests { theme.setLight() onNodeWithTag("stack") .assertIsDisplayed() - .assertPixelColorEquals(startX = 0, startY = 0, width = 4, height = 4, color = expectedLightColor) + .assertPixelColorEquals(expectedLightColor) theme.setDark() onNodeWithTag("stack") .assertIsDisplayed() - .assertPixelColorEquals(startX = 0, startY = 0, width = 4, height = 4, color = expectedDarkColor) + .assertPixelColorEquals(expectedDarkColor) } ) } From 65affb8af77af826e58799c9bef99b6c5eb5d0fd Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Thu, 12 Dec 2024 18:46:01 +0100 Subject: [PATCH 17/37] Fixes the border test. --- .../components/button/ButtonComponentView.kt | 4 ++-- .../components/properties/BorderStyle.kt | 10 ++++++---- .../components/stack/StackComponentView.kt | 12 +++++++----- .../components/style/StackComponentStyle.kt | 4 ++-- .../ui/revenuecatui/components/style/StyleFactory.kt | 3 +-- 5 files changed, 18 insertions(+), 15 deletions(-) diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/button/ButtonComponentView.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/button/ButtonComponentView.kt index 21fbdd2dd3..6e159615d3 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/button/ButtonComponentView.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/button/ButtonComponentView.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.revenuecat.purchases.paywalls.components.properties.Border import com.revenuecat.purchases.paywalls.components.properties.ColorInfo import com.revenuecat.purchases.paywalls.components.properties.ColorScheme import com.revenuecat.purchases.paywalls.components.properties.Dimension @@ -22,7 +23,6 @@ import com.revenuecat.purchases.paywalls.components.properties.Padding import com.revenuecat.purchases.paywalls.components.properties.Size import com.revenuecat.purchases.paywalls.components.properties.SizeConstraint.Fit import com.revenuecat.purchases.ui.revenuecatui.components.PaywallAction -import com.revenuecat.purchases.ui.revenuecatui.components.properties.BorderStyle import com.revenuecat.purchases.ui.revenuecatui.components.properties.ColorStyle.Solid import com.revenuecat.purchases.ui.revenuecatui.components.properties.ShadowStyle import com.revenuecat.purchases.ui.revenuecatui.components.stack.StackComponentView @@ -78,7 +78,7 @@ private fun previewButtonComponentStyle( padding = PaddingValues(all = 16.dp), margin = PaddingValues(all = 16.dp), shape = RoundedCornerShape(size = 20.dp), - border = BorderStyle(width = 2.dp, color = Solid(Color.Blue)), + border = Border(width = 2.0, color = ColorScheme(light = ColorInfo.Hex(Color.Blue.toArgb()))), shadow = ShadowStyle(color = Solid(Color.Black), radius = 10.dp, x = 0.dp, y = 3.dp), ), action: PaywallAction = PaywallAction.RestorePurchases, diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/properties/BorderStyle.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/properties/BorderStyle.kt index b5e0e72abe..e50773a80c 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/properties/BorderStyle.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/properties/BorderStyle.kt @@ -17,8 +17,10 @@ internal data class BorderStyle( @Composable @JvmSynthetic -internal fun Border.toBorderStyle(): BorderStyle = - BorderStyle( - width = width.dp, - color = color.toColorStyle(), +internal fun rememberBorderStyle(border: Border): BorderStyle { + val colorStyle = rememberColorStyle(border.color) + return BorderStyle( + width = border.width.dp, + color = colorStyle, ) +} diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/stack/StackComponentView.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/stack/StackComponentView.kt index a9b3006898..ef06d681df 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/stack/StackComponentView.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/stack/StackComponentView.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.revenuecat.purchases.paywalls.components.properties.Border import com.revenuecat.purchases.paywalls.components.properties.ColorInfo import com.revenuecat.purchases.paywalls.components.properties.ColorScheme import com.revenuecat.purchases.paywalls.components.properties.Dimension @@ -39,9 +40,9 @@ import com.revenuecat.purchases.ui.revenuecatui.components.modifier.background import com.revenuecat.purchases.ui.revenuecatui.components.modifier.border import com.revenuecat.purchases.ui.revenuecatui.components.modifier.shadow import com.revenuecat.purchases.ui.revenuecatui.components.modifier.size -import com.revenuecat.purchases.ui.revenuecatui.components.properties.BorderStyle import com.revenuecat.purchases.ui.revenuecatui.components.properties.ColorStyle.Solid import com.revenuecat.purchases.ui.revenuecatui.components.properties.ShadowStyle +import com.revenuecat.purchases.ui.revenuecatui.components.properties.rememberBorderStyle import com.revenuecat.purchases.ui.revenuecatui.components.properties.rememberColorStyle import com.revenuecat.purchases.ui.revenuecatui.components.style.StackComponentStyle import com.revenuecat.purchases.ui.revenuecatui.components.style.TextComponentStyle @@ -55,6 +56,7 @@ internal fun StackComponentView( ) { if (style.visible) { val backgroundColorStyle = style.backgroundColor?.let { rememberColorStyle(scheme = it) } + val borderStyle = style.border?.let { rememberBorderStyle(border = it) } // Modifier irrespective of dimension. val commonModifier = remember(style) { @@ -63,7 +65,7 @@ internal fun StackComponentView( .applyIfNotNull(style.shadow) { shadow(it, style.shape) } .applyIfNotNull(backgroundColorStyle) { background(it, style.shape) } .clip(style.shape) - .applyIfNotNull(style.border) { border(it, style.shape) } + .applyIfNotNull(borderStyle) { border(it, style.shape) } .padding(style.padding) } @@ -128,7 +130,7 @@ private fun StackComponentView_Preview_Vertical() { padding = PaddingValues(all = 16.dp), margin = PaddingValues(all = 16.dp), shape = RoundedCornerShape(size = 20.dp), - border = BorderStyle(width = 2.dp, color = Solid(Color.Blue)), + border = Border(width = 2.0, color = ColorScheme(light = ColorInfo.Hex(Color.Blue.toArgb()))), shadow = ShadowStyle(color = Solid(Color.Black), radius = 10.dp, x = 0.dp, y = 3.dp), ), ) @@ -156,7 +158,7 @@ private fun StackComponentView_Preview_Horizontal() { padding = PaddingValues(all = 16.dp), margin = PaddingValues(all = 16.dp), shape = RoundedCornerShape(size = 20.dp), - border = BorderStyle(width = 2.dp, color = Solid(Color.Blue)), + border = Border(width = 2.0, color = ColorScheme(light = ColorInfo.Hex(Color.Blue.toArgb()))), shadow = ShadowStyle(color = Solid(Color.Black), radius = 30.dp, x = 0.dp, y = 5.dp), ), ) @@ -223,7 +225,7 @@ private fun StackComponentView_Preview_ZLayer() { padding = PaddingValues(all = 16.dp), margin = PaddingValues(all = 16.dp), shape = RoundedCornerShape(size = 20.dp), - border = BorderStyle(width = 2.dp, color = Solid(Color.Blue)), + border = Border(width = 2.0, color = ColorScheme(light = ColorInfo.Hex(Color.Blue.toArgb()))), shadow = ShadowStyle(color = Solid(Color.Black), radius = 20.dp, x = 5.dp, y = 5.dp), ), ) diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/style/StackComponentStyle.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/style/StackComponentStyle.kt index dfdad06e30..380f65f757 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/style/StackComponentStyle.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/style/StackComponentStyle.kt @@ -4,10 +4,10 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Immutable import androidx.compose.ui.graphics.Shape import androidx.compose.ui.unit.Dp +import com.revenuecat.purchases.paywalls.components.properties.Border import com.revenuecat.purchases.paywalls.components.properties.ColorScheme import com.revenuecat.purchases.paywalls.components.properties.Dimension import com.revenuecat.purchases.paywalls.components.properties.Size -import com.revenuecat.purchases.ui.revenuecatui.components.properties.BorderStyle import com.revenuecat.purchases.ui.revenuecatui.components.properties.ShadowStyle @Suppress("LongParameterList") @@ -32,7 +32,7 @@ internal class StackComponentStyle( @get:JvmSynthetic val shape: Shape, @get:JvmSynthetic - val border: BorderStyle?, + val border: Border?, @get:JvmSynthetic val shadow: ShadowStyle?, ) : ComponentStyle 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 ef097e902e..0f9b5be5a4 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 @@ -23,7 +23,6 @@ import com.revenuecat.purchases.ui.revenuecatui.components.buildPresentedPartial import com.revenuecat.purchases.ui.revenuecatui.components.ktx.string import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toPaddingValues import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toShape -import com.revenuecat.purchases.ui.revenuecatui.components.properties.toBorderStyle import com.revenuecat.purchases.ui.revenuecatui.components.properties.toShadowStyle import com.revenuecat.purchases.ui.revenuecatui.components.state.PackageContext import com.revenuecat.purchases.ui.revenuecatui.components.toPresentedOverrides @@ -99,7 +98,7 @@ internal class StyleFactory( padding = (partial?.padding ?: component.padding).toPaddingValues(), margin = (partial?.margin ?: component.margin).toPaddingValues(), shape = (partial?.shape ?: component.shape)?.toShape() ?: DEFAULT_SHAPE, - border = (partial?.border ?: component.border)?.toBorderStyle(), + border = (partial?.border ?: component.border), shadow = (partial?.shadow ?: component.shadow)?.toShadowStyle(), ) } From 453b5cf7177b6fbcc81f63637b8bbf2b89d102a7 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Thu, 12 Dec 2024 19:10:41 +0100 Subject: [PATCH 18/37] Fixes the shadow test. --- .../components/button/ButtonComponentView.kt | 10 +- .../components/modifier/Shadow.kt | 108 ++++++++++-------- .../components/properties/ShadowStyle.kt | 14 ++- .../components/stack/StackComponentView.kt | 28 ++++- .../components/style/StackComponentStyle.kt | 4 +- .../components/style/StyleFactory.kt | 7 +- 6 files changed, 101 insertions(+), 70 deletions(-) diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/button/ButtonComponentView.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/button/ButtonComponentView.kt index 6e159615d3..f7fdbcdc92 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/button/ButtonComponentView.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/button/ButtonComponentView.kt @@ -20,11 +20,10 @@ import com.revenuecat.purchases.paywalls.components.properties.FontSize import com.revenuecat.purchases.paywalls.components.properties.FontWeight import com.revenuecat.purchases.paywalls.components.properties.HorizontalAlignment import com.revenuecat.purchases.paywalls.components.properties.Padding +import com.revenuecat.purchases.paywalls.components.properties.Shadow import com.revenuecat.purchases.paywalls.components.properties.Size import com.revenuecat.purchases.paywalls.components.properties.SizeConstraint.Fit import com.revenuecat.purchases.ui.revenuecatui.components.PaywallAction -import com.revenuecat.purchases.ui.revenuecatui.components.properties.ColorStyle.Solid -import com.revenuecat.purchases.ui.revenuecatui.components.properties.ShadowStyle import com.revenuecat.purchases.ui.revenuecatui.components.stack.StackComponentView import com.revenuecat.purchases.ui.revenuecatui.components.style.ButtonComponentStyle import com.revenuecat.purchases.ui.revenuecatui.components.style.StackComponentStyle @@ -79,7 +78,12 @@ private fun previewButtonComponentStyle( margin = PaddingValues(all = 16.dp), shape = RoundedCornerShape(size = 20.dp), border = Border(width = 2.0, color = ColorScheme(light = ColorInfo.Hex(Color.Blue.toArgb()))), - shadow = ShadowStyle(color = Solid(Color.Black), radius = 10.dp, x = 0.dp, y = 3.dp), + shadow = Shadow( + color = ColorScheme(ColorInfo.Hex(Color.Black.toArgb())), + radius = 10.0, + x = 0.0, + y = 3.0, + ), ), action: PaywallAction = PaywallAction.RestorePurchases, actionHandler: (PaywallAction) -> Unit = {}, diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/modifier/Shadow.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/modifier/Shadow.kt index 26cd829e1a..714dd4adbd 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/modifier/Shadow.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/modifier/Shadow.kt @@ -37,7 +37,7 @@ import com.revenuecat.purchases.paywalls.components.properties.ColorScheme import com.revenuecat.purchases.paywalls.components.properties.Shadow import com.revenuecat.purchases.ui.revenuecatui.components.properties.ColorStyle import com.revenuecat.purchases.ui.revenuecatui.components.properties.ShadowStyle -import com.revenuecat.purchases.ui.revenuecatui.components.properties.toShadowStyle +import com.revenuecat.purchases.ui.revenuecatui.components.properties.rememberShadowStyle @JvmSynthetic @Stable @@ -87,14 +87,16 @@ private fun Shadow_Preview_Circle() { modifier = Modifier .requiredSize(100.dp) .shadow( - shadow = Shadow( - color = ColorScheme( - light = ColorInfo.Hex(Color.Black.toArgb()), + shadow = rememberShadowStyle( + Shadow( + color = ColorScheme( + light = ColorInfo.Hex(Color.Black.toArgb()), + ), + x = 5.0, + y = 5.0, + radius = 0.0, ), - x = 5.0, - y = 5.0, - radius = 0.0, - ).toShadowStyle(), + ), shape = shape, ) .background(Color.Red, shape = shape), @@ -116,14 +118,16 @@ private fun Shadow_Preview_Square() { modifier = Modifier .requiredSize(100.dp) .shadow( - shadow = Shadow( - color = ColorScheme( - light = ColorInfo.Hex(Color.Black.toArgb()), + shadow = rememberShadowStyle( + Shadow( + color = ColorScheme( + light = ColorInfo.Hex(Color.Black.toArgb()), + ), + x = 10.0, + y = 5.0, + radius = 20.0, ), - x = 10.0, - y = 5.0, - radius = 20.0, - ).toShadowStyle(), + ), shape = shape, ) .background(Color.Red, shape = shape), @@ -145,30 +149,32 @@ private fun Shadow_Preview_Gradient_CustomShape() { text = "GET UNLIMITED RGB", modifier = Modifier .shadow( - shadow = Shadow( - color = ColorScheme( - light = ColorInfo.Gradient.Linear( - degrees = 0f, - points = listOf( - ColorInfo.Gradient.Point( - color = Color.Red.toArgb(), - percent = 0.1f, - ), - ColorInfo.Gradient.Point( - color = Color.Green.toArgb(), - percent = 0.5f, - ), - ColorInfo.Gradient.Point( - color = Color.Blue.toArgb(), - percent = 0.9f, + shadow = rememberShadowStyle( + Shadow( + color = ColorScheme( + light = ColorInfo.Gradient.Linear( + degrees = 0f, + points = listOf( + ColorInfo.Gradient.Point( + color = Color.Red.toArgb(), + percent = 0.1f, + ), + ColorInfo.Gradient.Point( + color = Color.Green.toArgb(), + percent = 0.5f, + ), + ColorInfo.Gradient.Point( + color = Color.Blue.toArgb(), + percent = 0.9f, + ), ), ), ), + x = 0.0, + y = 6.0, + radius = 9.5, ), - x = 0.0, - y = 6.0, - radius = 9.5, - ).toShadowStyle(), + ), shape = shape, ) .background(Color.Black, shape = shape) @@ -196,14 +202,16 @@ private fun Shadow_Preview_Margin() { .padding(margin) .requiredSize(width = 50.dp, height = 50.dp) .shadow( - shadow = Shadow( - color = ColorScheme( - light = ColorInfo.Hex(Color.Black.toArgb()), + shadow = rememberShadowStyle( + Shadow( + color = ColorScheme( + light = ColorInfo.Hex(Color.Black.toArgb()), + ), + x = 0.0, + y = 5.0, + radius = 20.0, ), - x = 0.0, - y = 5.0, - radius = 20.0, - ).toShadowStyle(), + ), shape = shape, ) .background(Color.Red, shape) @@ -216,14 +224,16 @@ private fun Shadow_Preview_Margin() { .padding(margin) .requiredSize(width = 50.dp, height = 50.dp) .shadow( - shadow = Shadow( - color = ColorScheme( - light = ColorInfo.Hex(Color.Black.toArgb()), + shadow = rememberShadowStyle( + Shadow( + color = ColorScheme( + light = ColorInfo.Hex(Color.Black.toArgb()), + ), + x = 0.0, + y = 5.0, + radius = 20.0, ), - x = 0.0, - y = 5.0, - radius = 20.0, - ).toShadowStyle(), + ), shape = shape, ) .background(Color.Red, shape) diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/properties/ShadowStyle.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/properties/ShadowStyle.kt index 5343aabda5..0a6e6596b2 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/properties/ShadowStyle.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/properties/ShadowStyle.kt @@ -19,10 +19,12 @@ internal data class ShadowStyle( @JvmSynthetic @Composable -internal fun Shadow.toShadowStyle(): ShadowStyle = - ShadowStyle( - color = color.toColorStyle(), - radius = radius.dp, - x = x.dp, - y = y.dp, +internal fun rememberShadowStyle(shadow: Shadow): ShadowStyle { + val colorStyle = rememberColorStyle(shadow.color) + return ShadowStyle( + color = colorStyle, + radius = shadow.radius.dp, + x = shadow.x.dp, + y = shadow.y.dp, ) +} diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/stack/StackComponentView.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/stack/StackComponentView.kt index ef06d681df..5433589c15 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/stack/StackComponentView.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/stack/StackComponentView.kt @@ -26,6 +26,7 @@ import com.revenuecat.purchases.paywalls.components.properties.FontSize import com.revenuecat.purchases.paywalls.components.properties.FontWeight import com.revenuecat.purchases.paywalls.components.properties.HorizontalAlignment import com.revenuecat.purchases.paywalls.components.properties.Padding +import com.revenuecat.purchases.paywalls.components.properties.Shadow import com.revenuecat.purchases.paywalls.components.properties.Size import com.revenuecat.purchases.paywalls.components.properties.SizeConstraint.Fit import com.revenuecat.purchases.paywalls.components.properties.TwoDimensionalAlignment @@ -40,10 +41,9 @@ import com.revenuecat.purchases.ui.revenuecatui.components.modifier.background import com.revenuecat.purchases.ui.revenuecatui.components.modifier.border import com.revenuecat.purchases.ui.revenuecatui.components.modifier.shadow import com.revenuecat.purchases.ui.revenuecatui.components.modifier.size -import com.revenuecat.purchases.ui.revenuecatui.components.properties.ColorStyle.Solid -import com.revenuecat.purchases.ui.revenuecatui.components.properties.ShadowStyle import com.revenuecat.purchases.ui.revenuecatui.components.properties.rememberBorderStyle import com.revenuecat.purchases.ui.revenuecatui.components.properties.rememberColorStyle +import com.revenuecat.purchases.ui.revenuecatui.components.properties.rememberShadowStyle import com.revenuecat.purchases.ui.revenuecatui.components.style.StackComponentStyle import com.revenuecat.purchases.ui.revenuecatui.components.style.TextComponentStyle import com.revenuecat.purchases.ui.revenuecatui.extensions.applyIfNotNull @@ -57,12 +57,13 @@ internal fun StackComponentView( if (style.visible) { val backgroundColorStyle = style.backgroundColor?.let { rememberColorStyle(scheme = it) } val borderStyle = style.border?.let { rememberBorderStyle(border = it) } + val shadowStyle = style.shadow?.let { rememberShadowStyle(shadow = it) } // Modifier irrespective of dimension. val commonModifier = remember(style) { Modifier .padding(style.margin) - .applyIfNotNull(style.shadow) { shadow(it, style.shape) } + .applyIfNotNull(shadowStyle) { shadow(it, style.shape) } .applyIfNotNull(backgroundColorStyle) { background(it, style.shape) } .clip(style.shape) .applyIfNotNull(borderStyle) { border(it, style.shape) } @@ -131,7 +132,12 @@ private fun StackComponentView_Preview_Vertical() { margin = PaddingValues(all = 16.dp), shape = RoundedCornerShape(size = 20.dp), border = Border(width = 2.0, color = ColorScheme(light = ColorInfo.Hex(Color.Blue.toArgb()))), - shadow = ShadowStyle(color = Solid(Color.Black), radius = 10.dp, x = 0.dp, y = 3.dp), + shadow = Shadow( + color = ColorScheme(ColorInfo.Hex(Color.Black.toArgb())), + radius = 10.0, + x = 0.0, + y = 3.0, + ), ), ) } @@ -159,7 +165,12 @@ private fun StackComponentView_Preview_Horizontal() { margin = PaddingValues(all = 16.dp), shape = RoundedCornerShape(size = 20.dp), border = Border(width = 2.0, color = ColorScheme(light = ColorInfo.Hex(Color.Blue.toArgb()))), - shadow = ShadowStyle(color = Solid(Color.Black), radius = 30.dp, x = 0.dp, y = 5.dp), + shadow = Shadow( + color = ColorScheme(ColorInfo.Hex(Color.Black.toArgb())), + radius = 30.0, + x = 0.0, + y = 5.0, + ), ), ) } @@ -226,7 +237,12 @@ private fun StackComponentView_Preview_ZLayer() { margin = PaddingValues(all = 16.dp), shape = RoundedCornerShape(size = 20.dp), border = Border(width = 2.0, color = ColorScheme(light = ColorInfo.Hex(Color.Blue.toArgb()))), - shadow = ShadowStyle(color = Solid(Color.Black), radius = 20.dp, x = 5.dp, y = 5.dp), + shadow = Shadow( + color = ColorScheme(ColorInfo.Hex(Color.Black.toArgb())), + radius = 20.0, + x = 5.0, + y = 5.0, + ), ), ) } diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/style/StackComponentStyle.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/style/StackComponentStyle.kt index 380f65f757..9df71ee6b3 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/style/StackComponentStyle.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/style/StackComponentStyle.kt @@ -7,8 +7,8 @@ import androidx.compose.ui.unit.Dp import com.revenuecat.purchases.paywalls.components.properties.Border import com.revenuecat.purchases.paywalls.components.properties.ColorScheme import com.revenuecat.purchases.paywalls.components.properties.Dimension +import com.revenuecat.purchases.paywalls.components.properties.Shadow import com.revenuecat.purchases.paywalls.components.properties.Size -import com.revenuecat.purchases.ui.revenuecatui.components.properties.ShadowStyle @Suppress("LongParameterList") @Immutable @@ -34,5 +34,5 @@ internal class StackComponentStyle( @get:JvmSynthetic val border: Border?, @get:JvmSynthetic - val shadow: ShadowStyle?, + val shadow: Shadow?, ) : ComponentStyle 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 0f9b5be5a4..faee8134aa 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 @@ -23,7 +23,6 @@ import com.revenuecat.purchases.ui.revenuecatui.components.buildPresentedPartial import com.revenuecat.purchases.ui.revenuecatui.components.ktx.string import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toPaddingValues import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toShape -import com.revenuecat.purchases.ui.revenuecatui.components.properties.toShadowStyle import com.revenuecat.purchases.ui.revenuecatui.components.state.PackageContext import com.revenuecat.purchases.ui.revenuecatui.components.toPresentedOverrides import com.revenuecat.purchases.ui.revenuecatui.data.processed.VariableDataProvider @@ -94,12 +93,12 @@ internal class StyleFactory( dimension = partial?.dimension ?: component.dimension, size = partial?.size ?: component.size, spacing = (partial?.spacing ?: component.spacing ?: DEFAULT_SPACING).dp, - backgroundColor = (partial?.backgroundColor ?: component.backgroundColor), + backgroundColor = partial?.backgroundColor ?: component.backgroundColor, padding = (partial?.padding ?: component.padding).toPaddingValues(), margin = (partial?.margin ?: component.margin).toPaddingValues(), shape = (partial?.shape ?: component.shape)?.toShape() ?: DEFAULT_SHAPE, - border = (partial?.border ?: component.border), - shadow = (partial?.shadow ?: component.shadow)?.toShadowStyle(), + border = partial?.border ?: component.border, + shadow = partial?.shadow ?: component.shadow, ) } From 4c189ee45705f1c8960580012d565266ba6e073f Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Thu, 12 Dec 2024 19:15:00 +0100 Subject: [PATCH 19/37] Some cleanup. --- .../components/modifier/Border.kt | 77 ++++++++++--------- .../components/properties/BackgroundStyle.kt | 5 -- 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/modifier/Border.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/modifier/Border.kt index 14e94e182d..95ea42bab0 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/modifier/Border.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/modifier/Border.kt @@ -16,11 +16,12 @@ import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.revenuecat.purchases.paywalls.components.properties.Border import com.revenuecat.purchases.paywalls.components.properties.ColorInfo import com.revenuecat.purchases.paywalls.components.properties.ColorScheme import com.revenuecat.purchases.ui.revenuecatui.components.properties.BorderStyle import com.revenuecat.purchases.ui.revenuecatui.components.properties.ColorStyle -import com.revenuecat.purchases.ui.revenuecatui.components.properties.toColorStyle +import com.revenuecat.purchases.ui.revenuecatui.components.properties.rememberBorderStyle @JvmSynthetic @Stable @@ -117,27 +118,29 @@ private fun Border_Preview_LinearGradient(shape: Shape) { .requiredSize(100.dp) .background(Color.Red) .border( - border = BorderStyle( - width = 10.dp, - color = ColorScheme( - light = ColorInfo.Gradient.Linear( - degrees = -45f, - points = listOf( - ColorInfo.Gradient.Point( - color = Color.Cyan.toArgb(), - percent = 0.1f, - ), - ColorInfo.Gradient.Point( - color = Color(red = 0x00, green = 0x66, blue = 0xff).toArgb(), - percent = 0.3f, - ), - ColorInfo.Gradient.Point( - color = Color(red = 0xA0, green = 0x00, blue = 0xA0).toArgb(), - percent = 0.8f, + border = rememberBorderStyle( + Border( + width = 10.0, + color = ColorScheme( + light = ColorInfo.Gradient.Linear( + degrees = -45f, + points = listOf( + ColorInfo.Gradient.Point( + color = Color.Cyan.toArgb(), + percent = 0.1f, + ), + ColorInfo.Gradient.Point( + color = Color(red = 0x00, green = 0x66, blue = 0xff).toArgb(), + percent = 0.3f, + ), + ColorInfo.Gradient.Point( + color = Color(red = 0xA0, green = 0x00, blue = 0xA0).toArgb(), + percent = 0.8f, + ), ), ), ), - ).toColorStyle(), + ), ), shape = shape, ), @@ -152,26 +155,28 @@ private fun Border_Preview_RadialGradient(shape: Shape) { .requiredSize(100.dp) .background(Color.Red) .border( - border = BorderStyle( - width = 10.dp, - color = ColorScheme( - light = ColorInfo.Gradient.Radial( - points = listOf( - ColorInfo.Gradient.Point( - color = Color.Cyan.toArgb(), - percent = 0.8f, - ), - ColorInfo.Gradient.Point( - color = Color(red = 0x00, green = 0x66, blue = 0xff).toArgb(), - percent = 0.9f, - ), - ColorInfo.Gradient.Point( - color = Color(red = 0xA0, green = 0x00, blue = 0xA0).toArgb(), - percent = 0.96f, + border = rememberBorderStyle( + Border( + width = 10.0, + color = ColorScheme( + light = ColorInfo.Gradient.Radial( + points = listOf( + ColorInfo.Gradient.Point( + color = Color.Cyan.toArgb(), + percent = 0.8f, + ), + ColorInfo.Gradient.Point( + color = Color(red = 0x00, green = 0x66, blue = 0xff).toArgb(), + percent = 0.9f, + ), + ColorInfo.Gradient.Point( + color = Color(red = 0xA0, green = 0x00, blue = 0xA0).toArgb(), + percent = 0.96f, + ), ), ), ), - ).toColorStyle(), + ), ), shape = shape, ), diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/properties/BackgroundStyle.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/properties/BackgroundStyle.kt index 1673a0a60d..2b86335764 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/properties/BackgroundStyle.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/properties/BackgroundStyle.kt @@ -52,11 +52,6 @@ internal fun Background.toBackgroundStyle(): BackgroundStyle = } } -@JvmSynthetic -@Composable -internal fun ColorScheme.toBackgroundStyle(): BackgroundStyle = - BackgroundStyle.Color(color = toColorStyle()) - @Preview @Composable private fun Background_Preview_ColorHex() { From 914d9dac057f0d5f1ce5d84c44262b8376872515 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Fri, 13 Dec 2024 15:11:01 +0100 Subject: [PATCH 20/37] Adds a regression test. --- .../components/text/TextComponentViewTests.kt | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) 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 56b6dbfbdc..8c984405bc 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 @@ -1,10 +1,16 @@ package com.revenuecat.purchases.ui.revenuecatui.components.text +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.testTag import androidx.compose.ui.test.SemanticsNodeInteraction import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.test.ext.junit.runners.AndroidJUnit4 import com.revenuecat.purchases.paywalls.components.TextComponent @@ -12,6 +18,9 @@ import com.revenuecat.purchases.paywalls.components.common.LocalizationData import com.revenuecat.purchases.paywalls.components.common.LocalizationKey import com.revenuecat.purchases.paywalls.components.properties.ColorInfo import com.revenuecat.purchases.paywalls.components.properties.ColorScheme +import com.revenuecat.purchases.paywalls.components.properties.FontSize +import com.revenuecat.purchases.paywalls.components.properties.Size +import com.revenuecat.purchases.paywalls.components.properties.SizeConstraint.Fit import com.revenuecat.purchases.ui.revenuecatui.assertions.assertPixelColorEquals import com.revenuecat.purchases.ui.revenuecatui.assertions.assertTextColorEquals import com.revenuecat.purchases.ui.revenuecatui.components.ComponentViewState @@ -23,6 +32,7 @@ import com.revenuecat.purchases.ui.revenuecatui.data.processed.VariableDataProvi import com.revenuecat.purchases.ui.revenuecatui.data.testdata.MockResourceProvider import com.revenuecat.purchases.ui.revenuecatui.helpers.getOrThrow import com.revenuecat.purchases.ui.revenuecatui.helpers.themeChangingTest +import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Rule import org.junit.Test @@ -135,6 +145,51 @@ class TextComponentViewTests { ) } + /** + * There's some interplay between a Material3 theme and our Markdown component. If both of these are present in the + * Compose tree, the font size in the Markdown component did not have any effect. This is fixed in #1981. + * Unfortunately this bug does not show up in Compose Previews. Hence this test to protect against regressions. + */ + @GraphicsMode(GraphicsMode.Mode.NATIVE) + @Config(shadows = [ShadowPixelCopy::class], sdk = [34]) + @Test + fun `Should properly set the font size in a Material3 theme`(): Unit = with(composeTestRule) { + // Arrange + val textId = localizationDictionary.keys.first() + val color = ColorScheme(light = ColorInfo.Hex(Color.Black.toArgb())) + val size = Size(Fit, Fit) + setContent { + val largeTextStyle = styleFactory.create( + TextComponent(text = textId, color = color, fontSize = FontSize.HEADING_L, size = size) + ).getOrThrow() as TextComponentStyle + val smallTextStyle = styleFactory.create( + TextComponent(text = textId, color = color, fontSize = FontSize.BODY_S, size = size) + ).getOrThrow() as TextComponentStyle + + // Act + MaterialTheme { + Column(modifier = Modifier.fillMaxSize()) { + TextComponentView(style = largeTextStyle, modifier = Modifier.testTag("large")) + TextComponentView(style = smallTextStyle, modifier = Modifier.testTag("small")) + } + } + } + + // Assert + val largeSize = onNodeWithTag("large") + .assertIsDisplayed() + .fetchSemanticsNode() + .size + + val smallSize = onNodeWithTag("small") + .assertIsDisplayed() + .fetchSemanticsNode() + .size + + assertThat(largeSize.height).isGreaterThan(smallSize.height) + assertThat(largeSize.width).isGreaterThan(smallSize.width) + } + /** * This is a very naive way of checking the background color: by just looking at the 16 top-left pixels. It works * for the particular test where it is used, because the color is solid and the text is transparent, but it From 9abcb6a0ce0edcb90a06cef942c4b57800058c39 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Fri, 13 Dec 2024 15:36:48 +0100 Subject: [PATCH 21/37] Excludes Paywalls V1 from new font size behavior. --- .../composables/IntroEligibilityStateView.kt | 1 + .../ui/revenuecatui/composables/Markdown.kt | 33 +++++++++++++++++-- .../ui/revenuecatui/templates/Template1.kt | 2 ++ .../ui/revenuecatui/templates/Template2.kt | 2 ++ .../ui/revenuecatui/templates/Template3.kt | 3 ++ .../ui/revenuecatui/templates/Template4.kt | 1 + .../ui/revenuecatui/templates/Template5.kt | 3 ++ .../ui/revenuecatui/templates/Template7.kt | 3 ++ 8 files changed, 45 insertions(+), 3 deletions(-) diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/composables/IntroEligibilityStateView.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/composables/IntroEligibilityStateView.kt index 71871d5279..9d2638f5af 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/composables/IntroEligibilityStateView.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/composables/IntroEligibilityStateView.kt @@ -46,6 +46,7 @@ internal fun IntroEligibilityStateView( textAlign = textAlign, allowLinks = allowLinks, textFillMaxWidth = true, + applyFontSizeToParagraph = false, modifier = modifier, ) } diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/composables/Markdown.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/composables/Markdown.kt index 74291376a0..5fa5bbb8f5 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/composables/Markdown.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/composables/Markdown.kt @@ -66,6 +66,9 @@ private val parser = Parser.builder() * @param allowLinks If true, links will be decorated and clickable. * @param textFillMaxWidth If true, the text will fill the maximum width available. This was used by paywalls V1 and * left to avoid unintended UI changes. + * @param applyFontSizeToParagraph If true, the provided [fontSize] will be applied to the annotated string used to + * build a Markdown paragraph from the [text]. This was not the case in Paywalls V1, but is needed for Paywalls V2. + * (See `TextComponentViewTests` for more info.) */ @SuppressWarnings("LongParameterList") @Composable @@ -80,7 +83,9 @@ internal fun Markdown( horizontalAlignment: Alignment.Horizontal = Alignment.Start, textAlign: TextAlign? = null, allowLinks: Boolean = true, - textFillMaxWidth: Boolean = false, // This is to support V1 paywalls + // The below parameters are used to avoid unintended changes to V1 paywalls. + textFillMaxWidth: Boolean = false, + applyFontSizeToParagraph: Boolean = true, ) { val root = parser.parse(text) as Document @@ -98,7 +103,18 @@ internal fun Markdown( horizontalAlignment = horizontalAlignment, modifier = modifier, ) { - MDDocument(root, color, style, fontSize, fontWeight, fontFamily, textAlign, allowLinks, textFillMaxWidth) + MDDocument( + root, + color, + style, + fontSize, + fontWeight, + fontFamily, + textAlign, + allowLinks, + textFillMaxWidth, + applyFontSizeToParagraph, + ) } } @@ -114,6 +130,7 @@ private fun MDDocument( textAlign: TextAlign?, allowLinks: Boolean, textFillMaxWidth: Boolean, + applyFontSizeToParagraph: Boolean, ) { MDBlockChildren( document, @@ -125,6 +142,7 @@ private fun MDDocument( textAlign, allowLinks, textFillMaxWidth, + applyFontSizeToParagraph, ) } @@ -140,6 +158,7 @@ private fun MDHeading( textAlign: TextAlign?, allowLinks: Boolean, textFillMaxWidth: Boolean, + applyFontSizeToParagraph: Boolean, modifier: Modifier = Modifier, ) { val overriddenStyle = when (heading.level) { @@ -161,6 +180,7 @@ private fun MDHeading( textAlign, allowLinks, textFillMaxWidth, + applyFontSizeToParagraph, ) return } @@ -197,12 +217,16 @@ private fun MDParagraph( textAlign: TextAlign?, allowLinks: Boolean, textFillMaxWidth: Boolean, + applyFontSizeToParagraph: Boolean, ) { Box { val styledText = buildAnnotatedString { pushStyle( style - .copy(fontWeight = fontWeight, fontSize = fontSize) + .copy( + fontWeight = fontWeight, + fontSize = if (applyFontSizeToParagraph) fontSize else style.fontSize, + ) .toSpanStyle(), ) appendMarkdownChildren(paragraph as Node, color, allowLinks) @@ -419,6 +443,7 @@ private fun MDBlockChildren( textAlign: TextAlign?, allowLinks: Boolean, textFillMaxWidth: Boolean, + applyFontSizeToParagraph: Boolean, ) { var child = parent.firstChild while (child != null) { @@ -434,6 +459,7 @@ private fun MDBlockChildren( textAlign, allowLinks, textFillMaxWidth, + applyFontSizeToParagraph, ) is Paragraph -> MDParagraph( child, @@ -445,6 +471,7 @@ private fun MDBlockChildren( textAlign, allowLinks, textFillMaxWidth, + applyFontSizeToParagraph, ) is FencedCodeBlock -> MDFencedCodeBlock(child) is BulletList -> MDBulletList( diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/templates/Template1.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/templates/Template1.kt index ba92598e0a..8ce1092186 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/templates/Template1.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/templates/Template1.kt @@ -99,6 +99,7 @@ private fun ColumnScope.Template1MainContent(state: PaywallState.Loaded.Legacy) textAlign = TextAlign.Center, color = colors.text1, textFillMaxWidth = true, + applyFontSizeToParagraph = false, modifier = Modifier .padding( horizontal = UIConstant.defaultHorizontalPadding, @@ -116,6 +117,7 @@ private fun ColumnScope.Template1MainContent(state: PaywallState.Loaded.Legacy) fontWeight = FontWeight.Normal, textAlign = TextAlign.Center, textFillMaxWidth = true, + applyFontSizeToParagraph = false, modifier = Modifier .padding( horizontal = UIConstant.defaultHorizontalPadding, diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/templates/Template2.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/templates/Template2.kt index dbbd69994f..0a6d316525 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/templates/Template2.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/templates/Template2.kt @@ -269,6 +269,7 @@ private fun Title( text = state.selectedLocalization.title, color = state.templateConfiguration.getCurrentColors().text1, textFillMaxWidth = true, + applyFontSizeToParagraph = false, modifier = childModifier, ) } @@ -286,6 +287,7 @@ private fun Subtitle( text = state.selectedLocalization.subtitle ?: "", color = state.templateConfiguration.getCurrentColors().text1, textFillMaxWidth = true, + applyFontSizeToParagraph = false, modifier = childModifier, ) } diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/templates/Template3.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/templates/Template3.kt index 04fea94366..ee988b24af 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/templates/Template3.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/templates/Template3.kt @@ -157,6 +157,7 @@ private fun Title( text = state.selectedLocalization.title, color = state.templateConfiguration.getCurrentColors().text1, textFillMaxWidth = true, + applyFontSizeToParagraph = false, ) } @@ -224,6 +225,7 @@ private fun Feature( text = feature.title, color = colors.text1, textFillMaxWidth = true, + applyFontSizeToParagraph = false, ) feature.content?.let { content -> Markdown( @@ -233,6 +235,7 @@ private fun Feature( text = content, color = colors.text2, textFillMaxWidth = true, + applyFontSizeToParagraph = false, ) } } diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/templates/Template4.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/templates/Template4.kt index 1e940d6ba0..8ead0d7501 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/templates/Template4.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/templates/Template4.kt @@ -164,6 +164,7 @@ private fun Template4MainContent( text = localizedConfig.title, color = colors.text1, textFillMaxWidth = true, + applyFontSizeToParagraph = false, modifier = Modifier.padding(horizontal = UIConstant.defaultHorizontalPadding), ) } diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/templates/Template5.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/templates/Template5.kt index 39bcb543a1..391aa231db 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/templates/Template5.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/templates/Template5.kt @@ -248,6 +248,7 @@ private fun ColumnScope.Title( text = state.selectedLocalization.title, color = state.templateConfiguration.getCurrentColors().text1, textFillMaxWidth = true, + applyFontSizeToParagraph = false, modifier = Modifier .fillMaxWidth(), ) @@ -303,6 +304,7 @@ private fun Feature( text = feature.title, color = colors.text1, textFillMaxWidth = true, + applyFontSizeToParagraph = false, ) feature.content?.let { content -> Markdown( @@ -312,6 +314,7 @@ private fun Feature( text = content, color = colors.text2, textFillMaxWidth = true, + applyFontSizeToParagraph = false, ) } } diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/templates/Template7.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/templates/Template7.kt index 24e640d179..415831da36 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/templates/Template7.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/templates/Template7.kt @@ -348,6 +348,7 @@ private fun ColumnScope.Title( text = localization.title, color = colorForTier.text1, textFillMaxWidth = true, + applyFontSizeToParagraph = false, modifier = Modifier .fillMaxWidth(), ) @@ -423,6 +424,7 @@ private fun Feature( text = feature.title, color = colors.text1, textFillMaxWidth = true, + applyFontSizeToParagraph = false, ) feature.content?.let { content -> Markdown( @@ -432,6 +434,7 @@ private fun Feature( text = content, color = colors.text2, textFillMaxWidth = true, + applyFontSizeToParagraph = false, ) } } From 43f1ecf9770f2d4d6621d916ed83480c7b1282d7 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Fri, 13 Dec 2024 16:05:54 +0100 Subject: [PATCH 22/37] TextComponentStyle no longer needs a Composable context to be created. --- .../revenuecatui/components/style/TextComponentStyle.kt | 8 ++------ .../ui/revenuecatui/components/text/TextComponentView.kt | 3 ++- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/style/TextComponentStyle.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/style/TextComponentStyle.kt index 2381d1e4ae..88a973a46a 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/style/TextComponentStyle.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/style/TextComponentStyle.kt @@ -1,7 +1,6 @@ package com.revenuecat.purchases.ui.revenuecatui.components.style import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.ui.Alignment import androidx.compose.ui.text.font.DeviceFontFamilyName @@ -9,7 +8,6 @@ import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.TextUnit import com.revenuecat.purchases.paywalls.components.properties.ColorScheme import com.revenuecat.purchases.paywalls.components.properties.FontSize import com.revenuecat.purchases.paywalls.components.properties.HorizontalAlignment @@ -19,7 +17,6 @@ import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toAlignment import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toFontWeight import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toPaddingValues import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toTextAlign -import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toTextUnit import com.revenuecat.purchases.paywalls.components.properties.FontWeight as RcFontWeight @Suppress("LongParameterList") @@ -32,7 +29,7 @@ internal class TextComponentStyle private constructor( @get:JvmSynthetic val color: ColorScheme, @get:JvmSynthetic - val fontSize: TextUnit, + val fontSize: FontSize, @get:JvmSynthetic val fontWeight: FontWeight?, @get:JvmSynthetic @@ -55,7 +52,6 @@ internal class TextComponentStyle private constructor( @Suppress("LongParameterList") @JvmSynthetic - @Composable operator fun invoke( visible: Boolean, text: String, @@ -76,7 +72,7 @@ internal class TextComponentStyle private constructor( visible = visible, text = text, color = color, - fontSize = fontSize.toTextUnit(), + fontSize = fontSize, fontWeight = weight, fontFamily = fontFamily?.let { SystemFontFamily(it, weight) }, textAlign = textAlign.toTextAlign(), diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/text/TextComponentView.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/text/TextComponentView.kt index 7d2c5fd44b..e50d58fc7b 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/text/TextComponentView.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/text/TextComponentView.kt @@ -23,6 +23,7 @@ import com.revenuecat.purchases.paywalls.components.properties.Size import com.revenuecat.purchases.paywalls.components.properties.SizeConstraint import com.revenuecat.purchases.paywalls.components.properties.SizeConstraint.Fill import com.revenuecat.purchases.paywalls.components.properties.SizeConstraint.Fit +import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toTextUnit import com.revenuecat.purchases.ui.revenuecatui.components.modifier.background import com.revenuecat.purchases.ui.revenuecatui.components.modifier.size import com.revenuecat.purchases.ui.revenuecatui.components.properties.ColorStyle @@ -61,7 +62,7 @@ internal fun TextComponentView( .applyIfNotNull(backgroundColorStyle) { background(it) } .padding(style.padding), color = color, - fontSize = style.fontSize, + fontSize = style.fontSize.toTextUnit(), fontWeight = style.fontWeight, fontFamily = style.fontFamily, horizontalAlignment = style.horizontalAlignment, From 412312e4aa697916d61d632859ab31cbfdf8ca5c Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Fri, 13 Dec 2024 16:16:32 +0100 Subject: [PATCH 23/37] TextComponentStyle just has a single constructor now. --- .../components/SystemFontFamily.kt | 21 +++++++ .../components/button/ButtonComponentView.kt | 14 +++-- .../components/stack/StackComponentView.kt | 43 ++++++------- .../components/style/StyleFactory.kt | 17 ++++-- .../components/style/TextComponentStyle.kt | 60 +------------------ .../components/text/TextComponentView.kt | 36 ++++++----- 6 files changed, 88 insertions(+), 103 deletions(-) create mode 100644 ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/SystemFontFamily.kt diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/SystemFontFamily.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/SystemFontFamily.kt new file mode 100644 index 0000000000..b7a070d573 --- /dev/null +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/SystemFontFamily.kt @@ -0,0 +1,21 @@ +@file:JvmSynthetic + +package com.revenuecat.purchases.ui.revenuecatui.components + +import androidx.compose.ui.text.font.DeviceFontFamilyName +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight + +/** + * Get an Android system-installed font by [familyName]. + */ +@Suppress("FunctionName") +@JvmSynthetic +internal fun SystemFontFamily(familyName: String, weight: FontWeight?): FontFamily = + FontFamily( + Font( + familyName = DeviceFontFamilyName(familyName), + weight = weight ?: FontWeight.Normal, + ), + ) diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/button/ButtonComponentView.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/button/ButtonComponentView.kt index f7fdbcdc92..6dcf526754 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/button/ButtonComponentView.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/button/ButtonComponentView.kt @@ -24,6 +24,10 @@ import com.revenuecat.purchases.paywalls.components.properties.Shadow import com.revenuecat.purchases.paywalls.components.properties.Size import com.revenuecat.purchases.paywalls.components.properties.SizeConstraint.Fit import com.revenuecat.purchases.ui.revenuecatui.components.PaywallAction +import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toAlignment +import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toFontWeight +import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toPaddingValues +import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toTextAlign import com.revenuecat.purchases.ui.revenuecatui.components.stack.StackComponentView import com.revenuecat.purchases.ui.revenuecatui.components.style.ButtonComponentStyle import com.revenuecat.purchases.ui.revenuecatui.components.style.StackComponentStyle @@ -58,16 +62,16 @@ private fun previewButtonComponentStyle( light = ColorInfo.Hex(Color.Black.toArgb()), ), fontSize = FontSize.BODY_M, - fontWeight = FontWeight.REGULAR, + fontWeight = FontWeight.REGULAR.toFontWeight(), fontFamily = null, - textAlign = HorizontalAlignment.CENTER, - horizontalAlignment = HorizontalAlignment.CENTER, + textAlign = HorizontalAlignment.CENTER.toTextAlign(), + horizontalAlignment = HorizontalAlignment.CENTER.toAlignment(), backgroundColor = ColorScheme( light = ColorInfo.Hex(Color.Yellow.toArgb()), ), size = Size(width = Fit, height = Fit), - padding = Padding(top = 8.0, bottom = 8.0, leading = 8.0, trailing = 8.0), - margin = Padding(top = 0.0, bottom = 24.0, leading = 0.0, trailing = 24.0), + padding = Padding(top = 8.0, bottom = 8.0, leading = 8.0, trailing = 8.0).toPaddingValues(), + margin = Padding(top = 0.0, bottom = 24.0, leading = 0.0, trailing = 24.0).toPaddingValues(), ), ), dimension = Dimension.Vertical(alignment = HorizontalAlignment.CENTER, distribution = START), diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/stack/StackComponentView.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/stack/StackComponentView.kt index 5433589c15..bdb906f386 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/stack/StackComponentView.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/stack/StackComponentView.kt @@ -33,8 +33,11 @@ import com.revenuecat.purchases.paywalls.components.properties.TwoDimensionalAli import com.revenuecat.purchases.paywalls.components.properties.VerticalAlignment import com.revenuecat.purchases.ui.revenuecatui.components.ComponentView import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toAlignment +import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toFontWeight import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toHorizontalAlignmentOrNull import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toHorizontalArrangement +import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toPaddingValues +import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toTextAlign import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toVerticalAlignmentOrNull import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toVerticalArrangement import com.revenuecat.purchases.ui.revenuecatui.components.modifier.background @@ -195,17 +198,17 @@ private fun StackComponentView_Preview_ZLayer() { light = ColorInfo.Hex(Color.Black.toArgb()), ), fontSize = FontSize.BODY_M, - fontWeight = FontWeight.REGULAR, + fontWeight = FontWeight.REGULAR.toFontWeight(), fontFamily = null, - textAlign = HorizontalAlignment.CENTER, - horizontalAlignment = HorizontalAlignment.CENTER, + textAlign = HorizontalAlignment.CENTER.toTextAlign(), + horizontalAlignment = HorizontalAlignment.CENTER.toAlignment(), backgroundColor = ColorScheme( light = ColorInfo.Hex(Color.Yellow.toArgb()), dark = ColorInfo.Hex(Color.Red.toArgb()), ), size = Size(width = Fit, height = Fit), - padding = Padding(top = 8.0, bottom = 8.0, leading = 8.0, trailing = 8.0), - margin = Padding(top = 0.0, bottom = 24.0, leading = 0.0, trailing = 24.0), + padding = Padding(top = 8.0, bottom = 8.0, leading = 8.0, trailing = 8.0).toPaddingValues(), + margin = Padding(top = 0.0, bottom = 24.0, leading = 0.0, trailing = 24.0).toPaddingValues(), ), TextComponentStyle( visible = true, @@ -214,16 +217,16 @@ private fun StackComponentView_Preview_ZLayer() { light = ColorInfo.Hex(Color.Black.toArgb()), ), fontSize = FontSize.BODY_M, - fontWeight = FontWeight.REGULAR, + fontWeight = FontWeight.REGULAR.toFontWeight(), fontFamily = null, - textAlign = HorizontalAlignment.CENTER, - horizontalAlignment = HorizontalAlignment.CENTER, + textAlign = HorizontalAlignment.CENTER.toTextAlign(), + horizontalAlignment = HorizontalAlignment.CENTER.toAlignment(), backgroundColor = ColorScheme( light = ColorInfo.Hex(Color.Blue.toArgb()), ), size = Size(width = Fit, height = Fit), - padding = Padding(top = 8.0, bottom = 8.0, leading = 8.0, trailing = 8.0), - margin = Padding(top = 0.0, bottom = 0.0, leading = 0.0, trailing = 0.0), + padding = Padding(top = 8.0, bottom = 8.0, leading = 8.0, trailing = 8.0).toPaddingValues(), + margin = Padding(top = 0.0, bottom = 0.0, leading = 0.0, trailing = 0.0).toPaddingValues(), ), ), dimension = Dimension.ZLayer(alignment = TwoDimensionalAlignment.BOTTOM_TRAILING), @@ -257,16 +260,16 @@ private fun previewChildren() = listOf( light = ColorInfo.Hex(Color.Black.toArgb()), ), fontSize = FontSize.BODY_M, - fontWeight = FontWeight.REGULAR, + fontWeight = FontWeight.REGULAR.toFontWeight(), fontFamily = null, - textAlign = HorizontalAlignment.CENTER, - horizontalAlignment = HorizontalAlignment.CENTER, + textAlign = HorizontalAlignment.CENTER.toTextAlign(), + horizontalAlignment = HorizontalAlignment.CENTER.toAlignment(), backgroundColor = ColorScheme( light = ColorInfo.Hex(Color.Blue.toArgb()), ), size = Size(width = Fit, height = Fit), - padding = Padding(top = 8.0, bottom = 8.0, leading = 8.0, trailing = 8.0), - margin = Padding(top = 0.0, bottom = 0.0, leading = 0.0, trailing = 0.0), + padding = Padding(top = 8.0, bottom = 8.0, leading = 8.0, trailing = 8.0).toPaddingValues(), + margin = Padding(top = 0.0, bottom = 0.0, leading = 0.0, trailing = 0.0).toPaddingValues(), ), TextComponentStyle( visible = true, @@ -275,15 +278,15 @@ private fun previewChildren() = listOf( light = ColorInfo.Hex(Color.Black.toArgb()), ), fontSize = FontSize.BODY_M, - fontWeight = FontWeight.REGULAR, + fontWeight = FontWeight.REGULAR.toFontWeight(), fontFamily = null, - textAlign = HorizontalAlignment.CENTER, - horizontalAlignment = HorizontalAlignment.CENTER, + textAlign = HorizontalAlignment.CENTER.toTextAlign(), + horizontalAlignment = HorizontalAlignment.CENTER.toAlignment(), backgroundColor = ColorScheme( light = ColorInfo.Hex(Color.Blue.toArgb()), ), size = Size(width = Fit, height = Fit), - padding = Padding(top = 8.0, bottom = 8.0, leading = 8.0, trailing = 8.0), - margin = Padding(top = 0.0, bottom = 0.0, leading = 0.0, trailing = 0.0), + padding = Padding(top = 8.0, bottom = 8.0, leading = 8.0, trailing = 8.0).toPaddingValues(), + margin = Padding(top = 0.0, bottom = 0.0, leading = 0.0, trailing = 0.0).toPaddingValues(), ), ) 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 faee8134aa..71d36e1ce1 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 @@ -19,10 +19,14 @@ import com.revenuecat.purchases.ui.revenuecatui.components.ComponentViewState import com.revenuecat.purchases.ui.revenuecatui.components.LocalizedTextPartial import com.revenuecat.purchases.ui.revenuecatui.components.PresentedStackPartial import com.revenuecat.purchases.ui.revenuecatui.components.ScreenCondition +import com.revenuecat.purchases.ui.revenuecatui.components.SystemFontFamily import com.revenuecat.purchases.ui.revenuecatui.components.buildPresentedPartial import com.revenuecat.purchases.ui.revenuecatui.components.ktx.string +import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toAlignment +import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toFontWeight import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toPaddingValues import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toShape +import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toTextAlign import com.revenuecat.purchases.ui.revenuecatui.components.state.PackageContext import com.revenuecat.purchases.ui.revenuecatui.components.toPresentedOverrides import com.revenuecat.purchases.ui.revenuecatui.data.processed.VariableDataProvider @@ -118,6 +122,7 @@ internal class StyleFactory( ) { text, presentedPartial -> // Combine the text and PresentedPartial into a TextComponentStyle. val partial = presentedPartial?.partial + val weight = (partial?.fontWeight ?: component.fontWeight).toFontWeight() TextComponentStyle( visible = partial?.visible ?: true, @@ -129,14 +134,14 @@ internal class StyleFactory( ), color = partial?.color ?: component.color, fontSize = partial?.fontSize ?: component.fontSize, - fontWeight = partial?.fontWeight ?: component.fontWeight, - fontFamily = partial?.fontName ?: component.fontName, - textAlign = partial?.horizontalAlignment ?: component.horizontalAlignment, - horizontalAlignment = partial?.horizontalAlignment ?: component.horizontalAlignment, + fontWeight = weight, + fontFamily = (partial?.fontName ?: component.fontName)?.let { SystemFontFamily(it, weight) }, + textAlign = (partial?.horizontalAlignment ?: component.horizontalAlignment).toTextAlign(), + horizontalAlignment = (partial?.horizontalAlignment ?: component.horizontalAlignment).toAlignment(), backgroundColor = partial?.backgroundColor ?: component.backgroundColor, size = partial?.size ?: component.size, - padding = partial?.padding ?: component.padding, - margin = partial?.margin ?: component.margin, + padding = (partial?.padding ?: component.padding).toPaddingValues(), + margin = (partial?.margin ?: component.margin).toPaddingValues(), ) } diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/style/TextComponentStyle.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/style/TextComponentStyle.kt index 88a973a46a..ef6c2fb33f 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/style/TextComponentStyle.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/style/TextComponentStyle.kt @@ -3,25 +3,16 @@ package com.revenuecat.purchases.ui.revenuecatui.components.style import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Immutable import androidx.compose.ui.Alignment -import androidx.compose.ui.text.font.DeviceFontFamilyName -import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import com.revenuecat.purchases.paywalls.components.properties.ColorScheme import com.revenuecat.purchases.paywalls.components.properties.FontSize -import com.revenuecat.purchases.paywalls.components.properties.HorizontalAlignment -import com.revenuecat.purchases.paywalls.components.properties.Padding import com.revenuecat.purchases.paywalls.components.properties.Size -import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toAlignment -import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toFontWeight -import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toPaddingValues -import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toTextAlign -import com.revenuecat.purchases.paywalls.components.properties.FontWeight as RcFontWeight @Suppress("LongParameterList") @Immutable -internal class TextComponentStyle private constructor( +internal class TextComponentStyle( @get:JvmSynthetic val visible: Boolean, @get:JvmSynthetic @@ -46,51 +37,4 @@ internal class TextComponentStyle private constructor( val padding: PaddingValues, @get:JvmSynthetic val margin: PaddingValues, -) : ComponentStyle { - - companion object { - - @Suppress("LongParameterList") - @JvmSynthetic - operator fun invoke( - visible: Boolean, - text: String, - color: ColorScheme, - fontSize: FontSize, - fontWeight: RcFontWeight?, - fontFamily: String?, - textAlign: HorizontalAlignment, - horizontalAlignment: HorizontalAlignment, - backgroundColor: ColorScheme?, - size: Size, - padding: Padding, - margin: Padding, - ): TextComponentStyle { - val weight = fontWeight?.toFontWeight() - - return TextComponentStyle( - visible = visible, - text = text, - color = color, - fontSize = fontSize, - fontWeight = weight, - fontFamily = fontFamily?.let { SystemFontFamily(it, weight) }, - textAlign = textAlign.toTextAlign(), - horizontalAlignment = horizontalAlignment.toAlignment(), - backgroundColor = backgroundColor, - size = size, - padding = padding.toPaddingValues(), - margin = margin.toPaddingValues(), - ) - } - - @Suppress("FunctionName") - private fun SystemFontFamily(familyName: String, weight: FontWeight?): FontFamily = - FontFamily( - Font( - familyName = DeviceFontFamilyName(familyName), - weight = weight ?: FontWeight.Normal, - ), - ) - } -} +) : ComponentStyle diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/text/TextComponentView.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/text/TextComponentView.kt index e50d58fc7b..242015abac 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/text/TextComponentView.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/text/TextComponentView.kt @@ -23,6 +23,11 @@ import com.revenuecat.purchases.paywalls.components.properties.Size import com.revenuecat.purchases.paywalls.components.properties.SizeConstraint import com.revenuecat.purchases.paywalls.components.properties.SizeConstraint.Fill import com.revenuecat.purchases.paywalls.components.properties.SizeConstraint.Fit +import com.revenuecat.purchases.ui.revenuecatui.components.SystemFontFamily +import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toAlignment +import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toFontWeight +import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toPaddingValues +import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toTextAlign import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toTextUnit import com.revenuecat.purchases.ui.revenuecatui.components.modifier.background import com.revenuecat.purchases.ui.revenuecatui.components.modifier.size @@ -283,17 +288,20 @@ private fun previewTextComponentStyle( size: Size = Size(width = Fill, height = Fit), padding: Padding = zero, margin: Padding = zero, -) = TextComponentStyle( - visible = visible, - text = text, - color = color, - fontSize = fontSize, - fontWeight = fontWeight, - fontFamily = fontFamily, - textAlign = textAlign, - horizontalAlignment = horizontalAlignment, - backgroundColor = backgroundColor, - size = size, - padding = padding, - margin = margin, -) +): TextComponentStyle { + val weight = fontWeight.toFontWeight() + return TextComponentStyle( + visible = visible, + text = text, + color = color, + fontSize = fontSize, + fontWeight = weight, + fontFamily = fontFamily?.let { SystemFontFamily(it, weight) }, + textAlign = textAlign.toTextAlign(), + horizontalAlignment = horizontalAlignment.toAlignment(), + backgroundColor = backgroundColor, + size = size, + padding = padding.toPaddingValues(), + margin = margin.toPaddingValues(), + ) +} From 8bcb10d340cf214a094caf859be80edb4244c982 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Mon, 16 Dec 2024 11:21:20 +0100 Subject: [PATCH 24/37] Moves rememberProcessedText from StyleFactory to TextComponentView. --- .../components/style/StyleFactory.kt | 59 +----------- .../components/text/TextComponentView.kt | 94 ++++++++++++++++++- 2 files changed, 92 insertions(+), 61 deletions(-) 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 71d36e1ce1..0baf08aa16 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 @@ -1,9 +1,6 @@ package com.revenuecat.purchases.ui.revenuecatui.components.style import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.unit.dp import com.revenuecat.purchases.paywalls.components.ButtonComponent @@ -30,7 +27,6 @@ import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toTextAlign import com.revenuecat.purchases.ui.revenuecatui.components.state.PackageContext import com.revenuecat.purchases.ui.revenuecatui.components.toPresentedOverrides import com.revenuecat.purchases.ui.revenuecatui.data.processed.VariableDataProvider -import com.revenuecat.purchases.ui.revenuecatui.data.processed.VariableProcessor import com.revenuecat.purchases.ui.revenuecatui.errors.PaywallValidationError import com.revenuecat.purchases.ui.revenuecatui.helpers.NonEmptyList import com.revenuecat.purchases.ui.revenuecatui.helpers.Result @@ -106,7 +102,6 @@ internal class StyleFactory( ) } - @Composable private fun createTextComponentStyle( component: TextComponent, ): Result> = zipOrAccumulate( @@ -126,12 +121,7 @@ internal class StyleFactory( TextComponentStyle( visible = partial?.visible ?: true, - text = rememberProcessedText( - originalText = presentedPartial?.text ?: text, - packageContext = packageContext, - locale = locale, - variables = variables, - ), + text = presentedPartial?.text ?: text, color = partial?.color ?: component.color, fontSize = partial?.fontSize ?: component.fontSize, fontWeight = weight, @@ -144,51 +134,4 @@ internal class StyleFactory( margin = (partial?.margin ?: component.margin).toPaddingValues(), ) } - - /** - * Replaces any [variables] in the [originalText] with values based on the currently selected - * [package][PackageContext.selectedPackage] and [locale]. - */ - @Composable - private fun rememberProcessedText( - originalText: String, - packageContext: PackageContext, - variables: VariableDataProvider, - locale: Locale, - ): String { - val processedText by remember(packageContext, variables, locale) { - derivedStateOf { - packageContext.selectedPackage?.let { selectedPackage -> - val discount = discountPercentage( - pricePerMonthMicros = selectedPackage.product.pricePerMonth()?.amountMicros, - mostExpensiveMicros = packageContext.variableContext.mostExpensivePricePerMonthMicros, - ) - val variableContext: VariableProcessor.PackageContext = VariableProcessor.PackageContext( - discountRelativeToMostExpensivePerMonth = discount, - showZeroDecimalPlacePrices = packageContext.variableContext.showZeroDecimalPlacePrices, - ) - VariableProcessor.processVariables( - variableDataProvider = variables, - context = variableContext, - originalString = originalText, - rcPackage = selectedPackage, - locale = locale, - ) - } ?: originalText - } - } - - return processedText - } - - private fun discountPercentage(pricePerMonthMicros: Long?, mostExpensiveMicros: Long?): Double? { - if (pricePerMonthMicros == null || - mostExpensiveMicros == null || - mostExpensiveMicros <= pricePerMonthMicros - ) { - return null - } - - return (mostExpensiveMicros - pricePerMonthMicros) / mostExpensiveMicros.toDouble() - } } diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/text/TextComponentView.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/text/TextComponentView.kt index 242015abac..e2fafd456f 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/text/TextComponentView.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/text/TextComponentView.kt @@ -7,9 +7,14 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.widthIn import androidx.compose.material3.LocalTextStyle import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.revenuecat.purchases.paywalls.components.properties.ColorInfo @@ -33,15 +38,26 @@ import com.revenuecat.purchases.ui.revenuecatui.components.modifier.background import com.revenuecat.purchases.ui.revenuecatui.components.modifier.size import com.revenuecat.purchases.ui.revenuecatui.components.properties.ColorStyle import com.revenuecat.purchases.ui.revenuecatui.components.properties.rememberColorStyle +import com.revenuecat.purchases.ui.revenuecatui.components.state.PackageContext import com.revenuecat.purchases.ui.revenuecatui.components.style.TextComponentStyle import com.revenuecat.purchases.ui.revenuecatui.composables.Markdown +import com.revenuecat.purchases.ui.revenuecatui.data.processed.VariableDataProvider +import com.revenuecat.purchases.ui.revenuecatui.data.processed.VariableProcessor import com.revenuecat.purchases.ui.revenuecatui.extensions.applyIfNotNull +import com.revenuecat.purchases.ui.revenuecatui.helpers.toResourceProvider @Composable internal fun TextComponentView( style: TextComponentStyle, modifier: Modifier = Modifier, + // TODO Remove these default values + packageContext: PackageContext = PackageContext(null, PackageContext.VariableContext(emptyList())), + locale: Locale = Locale.current, ) { + val context = LocalContext.current + val variableDataProvider = remember { VariableDataProvider(context.toResourceProvider()) } + val text = rememberProcessedText(style.text, packageContext, variableDataProvider, locale) + val colorStyle = rememberColorStyle(scheme = style.color) val backgroundColorStyle = style.backgroundColor?.let { rememberColorStyle(scheme = it) } @@ -60,7 +76,7 @@ internal fun TextComponentView( if (style.visible) { Markdown( - text = style.text, + text = text, modifier = modifier .size(style.size, horizontalAlignment = style.horizontalAlignment) .padding(style.margin) @@ -77,6 +93,49 @@ internal fun TextComponentView( } } +@Composable +private fun rememberProcessedText( + originalText: String, + packageContext: PackageContext, + variables: VariableDataProvider, + locale: Locale, +): String { + val processedText by remember(packageContext, variables, locale) { + derivedStateOf { + packageContext.selectedPackage?.let { selectedPackage -> + val discount = discountPercentage( + pricePerMonthMicros = selectedPackage.product.pricePerMonth()?.amountMicros, + mostExpensiveMicros = packageContext.variableContext.mostExpensivePricePerMonthMicros, + ) + val variableContext: VariableProcessor.PackageContext = VariableProcessor.PackageContext( + discountRelativeToMostExpensivePerMonth = discount, + showZeroDecimalPlacePrices = packageContext.variableContext.showZeroDecimalPlacePrices, + ) + VariableProcessor.processVariables( + variableDataProvider = variables, + context = variableContext, + originalString = originalText, + rcPackage = selectedPackage, + locale = java.util.Locale.forLanguageTag(locale.toLanguageTag()), + ) + } ?: originalText + } + } + + return processedText +} + +private fun discountPercentage(pricePerMonthMicros: Long?, mostExpensiveMicros: Long?): Double? { + if (pricePerMonthMicros == null || + mostExpensiveMicros == null || + mostExpensiveMicros <= pricePerMonthMicros + ) { + return null + } + + return (mostExpensiveMicros - pricePerMonthMicros) / mostExpensiveMicros.toDouble() +} + @Preview(name = "Default") @Composable private fun TextComponentView_Preview_Default() { @@ -85,6 +144,8 @@ private fun TextComponentView_Preview_Default() { text = "Hello, world", color = ColorScheme(light = ColorInfo.Hex(Color.Black.toArgb())), ), + packageContext = previewPackageState(), + locale = Locale.current, ) } @@ -98,6 +159,8 @@ private fun TextComponentView_Preview_SerifFont() { fontFamily = "serif", size = Size(width = Fit, height = Fit), ), + packageContext = previewPackageState(), + locale = Locale.current, ) } @@ -111,6 +174,8 @@ private fun TextComponentView_Preview_SansSerifFont() { fontFamily = "sans-serif", size = Size(width = Fit, height = Fit), ), + packageContext = previewPackageState(), + locale = Locale.current, ) } @@ -124,6 +189,8 @@ private fun TextComponentView_Preview_MonospaceFont() { fontFamily = "monospace", size = Size(width = Fit, height = Fit), ), + packageContext = previewPackageState(), + locale = Locale.current, ) } @@ -137,6 +204,8 @@ private fun TextComponentView_Preview_CursiveFont() { fontFamily = "cursive", size = Size(width = Fit, height = Fit), ), + packageContext = previewPackageState(), + locale = Locale.current, ) } @@ -150,6 +219,8 @@ private fun TextComponentView_Preview_FontSize() { fontSize = FontSize.HEADING_L, size = Size(width = Fit, height = Fit), ), + packageContext = previewPackageState(), + locale = Locale.current, ) } @@ -163,8 +234,10 @@ private fun TextComponentView_Preview_HorizontalAlignment() { size = Size(width = Fit, height = Fit), horizontalAlignment = HorizontalAlignment.TRAILING, ), + packageContext = previewPackageState(), // Our width is Fit, but we are forced to be wider than our contents. modifier = Modifier.widthIn(min = 400.dp), + locale = Locale.current, ) } @@ -183,6 +256,8 @@ private fun TextComponentView_Preview_Customizations() { padding = Padding(top = 10.0, bottom = 10.0, leading = 20.0, trailing = 20.0), margin = Padding(top = 20.0, bottom = 20.0, leading = 10.0, trailing = 10.0), ), + packageContext = previewPackageState(), + locale = Locale.current, ) } @@ -195,6 +270,8 @@ private fun TextComponentView_Preview_Markdown() { "Click [here](https://revenuecat.com)", color = ColorScheme(light = ColorInfo.Hex(Color.Black.toArgb())), ), + packageContext = previewPackageState(), + locale = Locale.current, ) } @@ -231,8 +308,9 @@ private fun TextComponentView_Preview_LinearGradient() { size = Size(width = SizeConstraint.Fixed(200.toUInt()), height = Fit), padding = Padding(top = 10.0, bottom = 10.0, leading = 20.0, trailing = 20.0), margin = Padding(top = 20.0, bottom = 20.0, leading = 10.0, trailing = 10.0), - ), + packageContext = previewPackageState(), + locale = Locale.current, ) } @@ -268,8 +346,9 @@ private fun TextComponentView_Preview_RadialGradient() { size = Size(width = SizeConstraint.Fixed(200.toUInt()), height = Fit), padding = Padding(top = 10.0, bottom = 10.0, leading = 20.0, trailing = 20.0), margin = Padding(top = 20.0, bottom = 20.0, leading = 10.0, trailing = 10.0), - ), + packageContext = previewPackageState(), + locale = Locale.current, ) } @@ -305,3 +384,12 @@ private fun previewTextComponentStyle( margin = margin.toPaddingValues(), ) } + +private fun previewPackageState(): PackageContext = + PackageContext( + initialSelectedPackage = null, + initialVariableContext = PackageContext.VariableContext( + packages = emptyList(), + showZeroDecimalPlacePrices = true, + ), + ) From 524ae3b89e95b14047d85a0967a730a875a8cca1 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Mon, 16 Dec 2024 11:24:35 +0100 Subject: [PATCH 25/37] Removes unused StyleFactory parameters. --- .../components/LoadedPaywallComponents.kt | 14 -------------- .../revenuecatui/components/style/StyleFactory.kt | 7 ------- .../components/stack/StackComponentViewTests.kt | 13 ------------- .../components/style/StyleFactoryTests.kt | 13 ------------- .../components/text/TextComponentViewTests.kt | 13 ------------- 5 files changed, 60 deletions(-) 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 5c7bae5290..f54ea26894 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 @@ -12,7 +12,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.window.core.layout.WindowSizeClass import com.revenuecat.purchases.Offering @@ -47,12 +46,9 @@ import com.revenuecat.purchases.paywalls.components.properties.TwoDimensionalAli import com.revenuecat.purchases.paywalls.components.properties.TwoDimensionalAlignment.BOTTOM import com.revenuecat.purchases.ui.revenuecatui.components.modifier.background import com.revenuecat.purchases.ui.revenuecatui.components.properties.toBackgroundStyle -import com.revenuecat.purchases.ui.revenuecatui.components.state.PackageContext import com.revenuecat.purchases.ui.revenuecatui.components.style.StyleFactory import com.revenuecat.purchases.ui.revenuecatui.data.PaywallState -import com.revenuecat.purchases.ui.revenuecatui.data.processed.VariableDataProvider import com.revenuecat.purchases.ui.revenuecatui.helpers.getOrThrow -import com.revenuecat.purchases.ui.revenuecatui.helpers.toResourceProvider import java.net.URL import java.util.Locale @@ -61,7 +57,6 @@ internal fun LoadedPaywallComponents( state: PaywallState.Loaded.Components, modifier: Modifier = Modifier, ) { - val context = LocalContext.current val configuration = LocalConfiguration.current // Configured locales take precedence over the default one. val preferredIds = configuration.locales.mapToLocaleIds() + state.data.defaultLocaleIdentifier @@ -78,16 +73,7 @@ internal fun LoadedPaywallComponents( windowSize = windowSize, isEligibleForIntroOffer = false, componentState = ComponentViewState.DEFAULT, - packageContext = PackageContext( - initialSelectedPackage = null, - initialVariableContext = PackageContext.VariableContext( - packages = state.offering.availablePackages, - showZeroDecimalPlacePrices = true, - ), - ), localizationDictionary = localizationDictionary, - locale = locale, - variables = VariableDataProvider(context.toResourceProvider()), ) } 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 0baf08aa16..c231f695bb 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 @@ -24,9 +24,7 @@ import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toFontWeight import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toPaddingValues import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toShape import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toTextAlign -import com.revenuecat.purchases.ui.revenuecatui.components.state.PackageContext import com.revenuecat.purchases.ui.revenuecatui.components.toPresentedOverrides -import com.revenuecat.purchases.ui.revenuecatui.data.processed.VariableDataProvider import com.revenuecat.purchases.ui.revenuecatui.errors.PaywallValidationError import com.revenuecat.purchases.ui.revenuecatui.helpers.NonEmptyList import com.revenuecat.purchases.ui.revenuecatui.helpers.Result @@ -36,17 +34,12 @@ import com.revenuecat.purchases.ui.revenuecatui.helpers.mapOrAccumulate import com.revenuecat.purchases.ui.revenuecatui.helpers.nonEmptyListOf import com.revenuecat.purchases.ui.revenuecatui.helpers.orSuccessfullyNull import com.revenuecat.purchases.ui.revenuecatui.helpers.zipOrAccumulate -import java.util.Locale -@Suppress("LongParameterList") internal class StyleFactory( private val windowSize: ScreenCondition, private val isEligibleForIntroOffer: Boolean, private val componentState: ComponentViewState, - private val packageContext: PackageContext, private val localizationDictionary: LocalizationDictionary, - private val locale: Locale, - private val variables: VariableDataProvider, ) { private companion object { 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 04b1017937..f27f9fb2a9 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 @@ -27,11 +27,8 @@ import com.revenuecat.purchases.ui.revenuecatui.assertions.assertPixelColorEqual import com.revenuecat.purchases.ui.revenuecatui.assertions.assertPixelColorPercentage import com.revenuecat.purchases.ui.revenuecatui.components.ComponentViewState import com.revenuecat.purchases.ui.revenuecatui.components.ScreenCondition -import com.revenuecat.purchases.ui.revenuecatui.components.state.PackageContext import com.revenuecat.purchases.ui.revenuecatui.components.style.StackComponentStyle import com.revenuecat.purchases.ui.revenuecatui.components.style.StyleFactory -import com.revenuecat.purchases.ui.revenuecatui.data.processed.VariableDataProvider -import com.revenuecat.purchases.ui.revenuecatui.data.testdata.MockResourceProvider import com.revenuecat.purchases.ui.revenuecatui.helpers.getOrThrow import com.revenuecat.purchases.ui.revenuecatui.helpers.themeChangingTest import org.junit.Before @@ -41,7 +38,6 @@ import org.junit.runner.RunWith import org.robolectric.annotation.Config import org.robolectric.annotation.GraphicsMode import org.robolectric.shadows.ShadowPixelCopy -import java.util.Locale @GraphicsMode(GraphicsMode.Mode.NATIVE) @Config(shadows = [ShadowPixelCopy::class], sdk = [26]) @@ -59,16 +55,7 @@ class StackComponentViewTests { windowSize = ScreenCondition.COMPACT, isEligibleForIntroOffer = true, componentState = ComponentViewState.DEFAULT, - packageContext = PackageContext( - initialSelectedPackage = null, - initialVariableContext = PackageContext.VariableContext( - packages = emptyList(), - showZeroDecimalPlacePrices = false - ) - ), localizationDictionary = emptyMap(), - locale = Locale.US, - variables = VariableDataProvider(MockResourceProvider()) ) } diff --git a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/style/StyleFactoryTests.kt b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/style/StyleFactoryTests.kt index 4ad130fdba..71e47e17fa 100644 --- a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/style/StyleFactoryTests.kt +++ b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/style/StyleFactoryTests.kt @@ -13,16 +13,12 @@ import com.revenuecat.purchases.paywalls.components.properties.ColorInfo import com.revenuecat.purchases.paywalls.components.properties.ColorScheme import com.revenuecat.purchases.ui.revenuecatui.components.ComponentViewState import com.revenuecat.purchases.ui.revenuecatui.components.ScreenCondition -import com.revenuecat.purchases.ui.revenuecatui.components.state.PackageContext -import com.revenuecat.purchases.ui.revenuecatui.data.processed.VariableDataProvider -import com.revenuecat.purchases.ui.revenuecatui.data.testdata.MockResourceProvider import com.revenuecat.purchases.ui.revenuecatui.helpers.Result import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import java.util.Locale @RunWith(AndroidJUnit4::class) class StyleFactoryTests { @@ -48,16 +44,7 @@ class StyleFactoryTests { windowSize = ScreenCondition.COMPACT, isEligibleForIntroOffer = true, componentState = ComponentViewState.DEFAULT, - packageContext = PackageContext( - initialSelectedPackage = null, - initialVariableContext = PackageContext.VariableContext( - packages = emptyList(), - showZeroDecimalPlacePrices = false - ) - ), localizationDictionary = localizationDictionary, - locale = Locale.US, - variables = VariableDataProvider(MockResourceProvider()) ) } 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 8c984405bc..dbde2f4d7c 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 @@ -25,11 +25,8 @@ import com.revenuecat.purchases.ui.revenuecatui.assertions.assertPixelColorEqual import com.revenuecat.purchases.ui.revenuecatui.assertions.assertTextColorEquals import com.revenuecat.purchases.ui.revenuecatui.components.ComponentViewState import com.revenuecat.purchases.ui.revenuecatui.components.ScreenCondition -import com.revenuecat.purchases.ui.revenuecatui.components.state.PackageContext import com.revenuecat.purchases.ui.revenuecatui.components.style.StyleFactory import com.revenuecat.purchases.ui.revenuecatui.components.style.TextComponentStyle -import com.revenuecat.purchases.ui.revenuecatui.data.processed.VariableDataProvider -import com.revenuecat.purchases.ui.revenuecatui.data.testdata.MockResourceProvider import com.revenuecat.purchases.ui.revenuecatui.helpers.getOrThrow import com.revenuecat.purchases.ui.revenuecatui.helpers.themeChangingTest import org.assertj.core.api.Assertions.assertThat @@ -40,7 +37,6 @@ import org.junit.runner.RunWith import org.robolectric.annotation.Config import org.robolectric.annotation.GraphicsMode import org.robolectric.shadows.ShadowPixelCopy -import java.util.Locale @RunWith(AndroidJUnit4::class) class TextComponentViewTests { @@ -59,16 +55,7 @@ class TextComponentViewTests { windowSize = ScreenCondition.COMPACT, isEligibleForIntroOffer = true, componentState = ComponentViewState.DEFAULT, - packageContext = PackageContext( - initialSelectedPackage = null, - initialVariableContext = PackageContext.VariableContext( - packages = emptyList(), - showZeroDecimalPlacePrices = false - ) - ), localizationDictionary = localizationDictionary, - locale = Locale.US, - variables = VariableDataProvider(MockResourceProvider()) ) } From 7adc5d36fa96a4ab1d74185528aaaf3421063216 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Mon, 16 Dec 2024 11:32:54 +0100 Subject: [PATCH 26/37] StyleFactory no longer needs a Composable context. --- .../purchases/ui/revenuecatui/components/style/StyleFactory.kt | 3 --- 1 file changed, 3 deletions(-) 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 c231f695bb..fc9629bfe6 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 @@ -1,6 +1,5 @@ package com.revenuecat.purchases.ui.revenuecatui.components.style -import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.unit.dp import com.revenuecat.purchases.paywalls.components.ButtonComponent @@ -47,7 +46,6 @@ internal class StyleFactory( private val DEFAULT_SHAPE = RectangleShape } - @Composable fun create(component: PaywallComponent): Result> = when (component) { is ButtonComponent -> TODO("ButtonComponentStyle is not yet implemented.") @@ -59,7 +57,6 @@ internal class StyleFactory( is TextComponent -> createTextComponentStyle(component = component) } - @Composable private fun createStackComponentStyle( component: StackComponent, ): Result> = zipOrAccumulate( From 7ef7d469df0b871f61b80176850482bf9f7d8f20 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Mon, 16 Dec 2024 15:58:08 +0100 Subject: [PATCH 27/37] Locale and LocalizationDictionary are part of PaywallState now. --- .../components/common/Localization.kt | 3 ++ .../components/LoadedPaywallComponents.kt | 28 ++------------- .../ui/revenuecatui/data/PaywallState.kt | 35 +++++++++++++++++-- 3 files changed, 39 insertions(+), 27 deletions(-) diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/components/common/Localization.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/components/common/Localization.kt index 11e61f10f4..70a7d18f46 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/components/common/Localization.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/components/common/Localization.kt @@ -13,6 +13,9 @@ import kotlinx.serialization.descriptors.buildSerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder +/** + * @property value The language tag of this locale, with an underscore separating the language from the region. + */ @InternalRevenueCatAPI @Serializable @JvmInline 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 f54ea26894..7de75b9c9e 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 @@ -3,7 +3,6 @@ package com.revenuecat.purchases.ui.revenuecatui.components import android.content.res.Configuration -import android.os.LocaleList import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.runtime.Composable @@ -50,7 +49,6 @@ import com.revenuecat.purchases.ui.revenuecatui.components.style.StyleFactory import com.revenuecat.purchases.ui.revenuecatui.data.PaywallState import com.revenuecat.purchases.ui.revenuecatui.helpers.getOrThrow import java.net.URL -import java.util.Locale @Composable internal fun LoadedPaywallComponents( @@ -58,22 +56,17 @@ internal fun LoadedPaywallComponents( modifier: Modifier = Modifier, ) { val configuration = LocalConfiguration.current - // Configured locales take precedence over the default one. - val preferredIds = configuration.locales.mapToLocaleIds() + state.data.defaultLocaleIdentifier - // Find the first locale we have a LocalizationDictionary for. - val localeId = preferredIds.first { id -> state.data.componentsLocalizations.containsKey(id) } - val localizationDictionary = state.data.componentsLocalizations.getValue(localeId) - val locale = localeId.toLocale() + state.update(localeList = configuration.locales) val windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo().windowSizeClass val windowSize = ScreenCondition.from(windowSizeClass.windowWidthSizeClass) - val styleFactory = remember(locale, windowSize) { + val styleFactory = remember(state.locale, windowSize) { StyleFactory( windowSize = windowSize, isEligibleForIntroOffer = false, componentState = ComponentViewState.DEFAULT, - localizationDictionary = localizationDictionary, + localizationDictionary = state.localizationDictionary, ) } @@ -89,21 +82,6 @@ internal fun LoadedPaywallComponents( ) } -private fun LocaleList.mapToLocaleIds(): List { - val result = ArrayList(size()) - for (i in 0 until size()) { - val locale = get(i) - if (locale != null) result.add(locale.toLocaleId()) - } - return result -} - -private fun LocaleId.toLocale(): Locale = - Locale.forLanguageTag(value) - -private fun Locale.toLocaleId(): LocaleId = - LocaleId(toLanguageTag()) - @Suppress("LongMethod") @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO or Configuration.UI_MODE_TYPE_NORMAL) @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES or Configuration.UI_MODE_TYPE_NORMAL) 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 06d3e3b4f4..94f20b0d57 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 @@ -3,13 +3,21 @@ package com.revenuecat.purchases.ui.revenuecatui.data import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.text.intl.Locale +import androidx.compose.ui.text.intl.LocaleList import com.revenuecat.purchases.Offering +import com.revenuecat.purchases.paywalls.components.common.LocaleId import com.revenuecat.purchases.paywalls.components.common.PaywallComponentsData import com.revenuecat.purchases.ui.revenuecatui.data.processed.ProcessedLocalizedConfiguration import com.revenuecat.purchases.ui.revenuecatui.data.processed.TemplateConfiguration import com.revenuecat.purchases.ui.revenuecatui.helpers.Logger import com.revenuecat.purchases.ui.revenuecatui.isFullScreen +import android.os.LocaleList as FrameworkLocaleList internal sealed interface PaywallState { object Loading : PaywallState @@ -48,10 +56,33 @@ internal sealed interface PaywallState { } } - data class Components( + @Stable + class Components( override val offering: Offering, val data: PaywallComponentsData, - ) : Loaded + initialLocaleList: LocaleList = LocaleList.current, + ) : Loaded { + private var localeId by mutableStateOf(initialLocaleList.toLocaleId()) + + val localizationDictionary by derivedStateOf { data.componentsLocalizations.getValue(localeId) } + val locale by derivedStateOf { localeId.toLocale() } + + fun update(localeList: FrameworkLocaleList) { + localeId = LocaleList(localeList.toLanguageTags()).toLocaleId() + } + + private fun LocaleList.toLocaleId(): LocaleId = + // Configured locales take precedence over the default one. + map { it.toLocaleId() }.plus(data.defaultLocaleIdentifier) + // Find the first locale we have a LocalizationDictionary for. + .first { id -> data.componentsLocalizations.containsKey(id) } + + private fun LocaleId.toLocale(): Locale = + Locale(value.replace('_', '-')) + + private fun Locale.toLocaleId(): LocaleId = + LocaleId(toLanguageTag().replace('-', '_')) + } } } From ea77f24e5b3b144bfaa6cdf956e91ca7beddd81f Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Mon, 16 Dec 2024 15:58:26 +0100 Subject: [PATCH 28/37] Adds LoadedPaywallComponentsLocaleTests. --- .../LoadedPaywallComponentsLocaleTests.kt | 148 ++++++++++++++++++ .../helpers/localeChangingTest.kt | 69 ++++++++ 2 files changed, 217 insertions(+) create mode 100644 ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/LoadedPaywallComponentsLocaleTests.kt create mode 100644 ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/helpers/localeChangingTest.kt 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 new file mode 100644 index 0000000000..bcf261d9e4 --- /dev/null +++ b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/LoadedPaywallComponentsLocaleTests.kt @@ -0,0 +1,148 @@ +package com.revenuecat.purchases.ui.revenuecatui.components + +import android.app.Application +import android.content.pm.ActivityInfo +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.test.core.app.ApplicationProvider.getApplicationContext +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.revenuecat.purchases.Offering +import com.revenuecat.purchases.paywalls.components.StackComponent +import com.revenuecat.purchases.paywalls.components.TextComponent +import com.revenuecat.purchases.paywalls.components.common.Background +import com.revenuecat.purchases.paywalls.components.common.ComponentsConfig +import com.revenuecat.purchases.paywalls.components.common.LocaleId +import com.revenuecat.purchases.paywalls.components.common.LocalizationData +import com.revenuecat.purchases.paywalls.components.common.LocalizationKey +import com.revenuecat.purchases.paywalls.components.common.PaywallComponentsConfig +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.PaywallState +import com.revenuecat.purchases.ui.revenuecatui.helpers.localeChangingTest +import org.junit.Rule +import org.junit.Test +import org.junit.experimental.runners.Enclosed +import org.junit.rules.TestWatcher +import org.junit.runner.Description +import org.junit.runner.RunWith +import org.robolectric.RuntimeEnvironment +import org.robolectric.Shadows.shadowOf +import java.net.URL + +@RunWith(Enclosed::class) +internal class LoadedPaywallComponentsLocaleTests { + private companion object { + private val localizationKey = LocalizationKey("hello-world") + + const val EXPECTED_TEXT_EN = "Hello, world!" + const val EXPECTED_TEXT_NL = "Hallo, wereld!" + + val paywallComponents = PaywallComponentsData( + templateName = "template", + assetBaseURL = URL("https://assets.pawwalls.com"), + componentsConfig = ComponentsConfig( + base = PaywallComponentsConfig( + stack = StackComponent( + components = listOf( + TextComponent( + text = localizationKey, + color = ColorScheme(light = ColorInfo.Hex(Color.Black.toArgb())) + ) + ) + ), + background = Background.Color(ColorScheme(light = ColorInfo.Hex(Color.White.toArgb()))), + stickyFooter = null, + ), + ), + componentsLocalizations = mapOf( + LocaleId("en_US") to mapOf( + localizationKey to LocalizationData.Text(EXPECTED_TEXT_EN), + ), + LocaleId("nl_NL") to mapOf( + localizationKey to LocalizationData.Text(EXPECTED_TEXT_NL), + ), + ), + defaultLocaleIdentifier = LocaleId("en_US"), + ) + val offering = Offering( + identifier = "identifier", + serverDescription = "serverDescription", + metadata = emptyMap(), + availablePackages = emptyList(), + paywallComponents = paywallComponents, + ) + } + + @RunWith(AndroidJUnit4::class) + class WithActivityRecreationTests { + + @get:Rule(order = 1) + val addActivityToRobolectricRule = object : TestWatcher() { + override fun starting(description: Description?) { + super.starting(description) + val appContext: Application = getApplicationContext() + val activityInfo = ActivityInfo().apply { + name = TestActivity::class.java.name + packageName = appContext.packageName + } + shadowOf(appContext.packageManager).addOrUpdateActivity(activityInfo) + } + } + + @get:Rule(order = 2) + internal val composeTestRule = createAndroidComposeRule() + + @Test + fun `Should propagate locale changes after Activity recreation`(): Unit = with(composeTestRule) { + // Assert that the locale is en_US at first. + onNodeWithText(EXPECTED_TEXT_EN) + .assertIsDisplayed() + // This changes the locale to nl_NL and restarts the Activity. + RuntimeEnvironment.setQualifiers("+nl-rNL") + // Assert that the nl_NL text is now displayed. + onNodeWithText(EXPECTED_TEXT_NL) + .assertIsDisplayed() + } + + class TestActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val state = PaywallState.Loaded.Components(offering, paywallComponents) + setContent { LoadedPaywallComponents(state = state) } + } + } + } + + @RunWith(AndroidJUnit4::class) + class WithoutActivityRecreationTests { + + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun `Should propagate locale changes without Activity recreation`(): Unit = with(composeTestRule) { + localeChangingTest( + arrange = { PaywallState.Loaded.Components(offering, paywallComponents) }, + act = { state -> LoadedPaywallComponents(state = state) }, + assert = { localeController -> + localeController.setLocale("en-US") + onNodeWithText(EXPECTED_TEXT_EN) + .assertIsDisplayed() + + localeController.setLocale("nl-NL") + onNodeWithText(EXPECTED_TEXT_NL) + .assertIsDisplayed() + } + ) + + } + } +} diff --git a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/helpers/localeChangingTest.kt b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/helpers/localeChangingTest.kt new file mode 100644 index 0000000000..c25041ae66 --- /dev/null +++ b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/helpers/localeChangingTest.kt @@ -0,0 +1,69 @@ +package com.revenuecat.purchases.ui.revenuecatui.helpers + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +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.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.test.junit4.ComposeContentTestRule +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performTextReplacement +import java.util.Locale + +/** + * Test Composable behavior across locale changes without recreating the Activity. This scenario occurs if the Activity + * has both `locale` and `layoutDirection` defined in the `configChanges` attribute in the manifest. + * + * @param arrange Return any setup data that needs to be built outside of the locale-dependent content. + * @param act Build the Composable content that is expected to react to locale changes here. Receives the return value + * from [arrange]. + * @param assert Assert the content built in [act]. The provided [LocaleController] can be used to control the active + * theme. + */ +internal fun ComposeContentTestRule.localeChangingTest( + arrange: @Composable () -> T, + act: @Composable (T) -> Unit, + assert: ComposeTestRule.(LocaleController) -> Unit, +) { + setContent { + val baseConfiguration: Configuration = LocalConfiguration.current + var languageTag by mutableStateOf(baseConfiguration.locales[0].toLanguageTag()) + val configuration by remember { + derivedStateOf { Configuration(baseConfiguration).apply { setLocale(Locale.forLanguageTag(languageTag)) } } + } + + val arrangeResult = arrange() + + CompositionLocalProvider(LocalConfiguration provides configuration) { + // The content under test, and a TextField to change the locale. + Column { + act(arrangeResult) + TextField( + value = languageTag, + onValueChange = { languageTag = it }, + modifier = Modifier.testTag("languageTag") + ) + } + } + } + + assert(LocaleController(this)) +} + +internal class LocaleController(private val composeTestRule: ComposeTestRule) { + fun setLocale(languageTag: String) { + composeTestRule + .onNodeWithTag("languageTag") + .performTextReplacement(text = languageTag) + } + +} From 8237323d188903ebc832dda54ab2e3c39e942100 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Mon, 16 Dec 2024 16:06:00 +0100 Subject: [PATCH 29/37] Adds isEligibleForIntroOffer to PaywallState. --- .../purchases/ui/revenuecatui/data/PaywallState.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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 94f20b0d57..413cd18f33 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 @@ -61,14 +61,19 @@ internal sealed interface PaywallState { override val offering: Offering, val data: PaywallComponentsData, initialLocaleList: LocaleList = LocaleList.current, + initialIsEligibleForIntroOffer: Boolean = false, ) : Loaded { private var localeId by mutableStateOf(initialLocaleList.toLocaleId()) val localizationDictionary by derivedStateOf { data.componentsLocalizations.getValue(localeId) } val locale by derivedStateOf { localeId.toLocale() } - fun update(localeList: FrameworkLocaleList) { - localeId = LocaleList(localeList.toLanguageTags()).toLocaleId() + var isEligibleForIntroOffer by mutableStateOf(initialIsEligibleForIntroOffer) + private set + + fun update(localeList: FrameworkLocaleList? = null, isEligibleForIntroOffer: Boolean? = null) { + if (localeList != null) localeId = LocaleList(localeList.toLanguageTags()).toLocaleId() + if (isEligibleForIntroOffer != null) this.isEligibleForIntroOffer = isEligibleForIntroOffer } private fun LocaleList.toLocaleId(): LocaleId = From 68f3c3809243efb7c32b2062c23044a8d695bb32 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Mon, 16 Dec 2024 16:06:49 +0100 Subject: [PATCH 30/37] Removes braces from a Preview annotation to please lint. --- .../ui/revenuecatui/components/LoadedPaywallComponents.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 7de75b9c9e..a987d71a29 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 @@ -162,7 +162,7 @@ private fun LoadedPaywallComponents_Preview() { } @Suppress("LongMethod") -@Preview() +@Preview @Composable private fun LoadedPaywallComponents_Preview_Bless() { val textColor = ColorScheme( From f18ea32929384b0fd5f32a828f046aeadc9d02b0 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Mon, 16 Dec 2024 17:10:26 +0100 Subject: [PATCH 31/37] Moves PackageContext to PaywallState. --- .../ui/revenuecatui/data/PaywallState.kt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) 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 413cd18f33..65a6a1afa0 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 @@ -11,6 +11,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.intl.LocaleList import com.revenuecat.purchases.Offering +import com.revenuecat.purchases.Package import com.revenuecat.purchases.paywalls.components.common.LocaleId import com.revenuecat.purchases.paywalls.components.common.PaywallComponentsData import com.revenuecat.purchases.ui.revenuecatui.data.processed.ProcessedLocalizedConfiguration @@ -62,6 +63,7 @@ internal sealed interface PaywallState { val data: PaywallComponentsData, initialLocaleList: LocaleList = LocaleList.current, initialIsEligibleForIntroOffer: Boolean = false, + initialSelectedPackage: Package? = null, ) : Loaded { private var localeId by mutableStateOf(initialLocaleList.toLocaleId()) @@ -70,12 +72,22 @@ internal sealed interface PaywallState { var isEligibleForIntroOffer by mutableStateOf(initialIsEligibleForIntroOffer) private set + var selectedPackage by mutableStateOf(initialSelectedPackage) + private set + + // TODO Actually determine this. + val showZeroDecimalPlacePrices: Boolean = true + val mostExpensivePricePerMonthMicros: Long? = offering.availablePackages.mostExpensivePricePerMonthMicros() fun update(localeList: FrameworkLocaleList? = null, isEligibleForIntroOffer: Boolean? = null) { if (localeList != null) localeId = LocaleList(localeList.toLanguageTags()).toLocaleId() if (isEligibleForIntroOffer != null) this.isEligibleForIntroOffer = isEligibleForIntroOffer } + fun update(selectedPackage: Package?) { + this.selectedPackage = selectedPackage + } + private fun LocaleList.toLocaleId(): LocaleId = // Configured locales take precedence over the default one. map { it.toLocaleId() }.plus(data.defaultLocaleIdentifier) @@ -87,6 +99,13 @@ internal sealed interface PaywallState { private fun Locale.toLocaleId(): LocaleId = LocaleId(toLanguageTag().replace('-', '_')) + + private fun List.mostExpensivePricePerMonthMicros(): Long? = + asSequence() + .map { pkg -> pkg.product } + .mapNotNull { product -> product.pricePerMonth() } + .maxByOrNull { price -> price.amountMicros } + ?.amountMicros } } } From 81bb7a6e46512b66c7ae25241470ff78811957cc Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Mon, 16 Dec 2024 17:17:29 +0100 Subject: [PATCH 32/37] Renames VariableContextTests to MostExpensivePricePerMonthMicrosTests. --- ... MostExpensivePricePerMonthMicrosTests.kt} | 54 ++++++++++++++++--- 1 file changed, 46 insertions(+), 8 deletions(-) rename ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/state/{VariableContextTests.kt => MostExpensivePricePerMonthMicrosTests.kt} (71%) diff --git a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/state/VariableContextTests.kt b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/state/MostExpensivePricePerMonthMicrosTests.kt similarity index 71% rename from ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/state/VariableContextTests.kt rename to ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/state/MostExpensivePricePerMonthMicrosTests.kt index 740398db39..e3ce942c7f 100644 --- a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/state/VariableContextTests.kt +++ b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/state/MostExpensivePricePerMonthMicrosTests.kt @@ -1,17 +1,29 @@ package com.revenuecat.purchases.ui.revenuecatui.components.state +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import com.revenuecat.purchases.Offering import com.revenuecat.purchases.Package 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.ui.revenuecatui.components.state.PackageContext.VariableContext +import com.revenuecat.purchases.paywalls.components.StackComponent +import com.revenuecat.purchases.paywalls.components.common.Background +import com.revenuecat.purchases.paywalls.components.common.ComponentsConfig +import com.revenuecat.purchases.paywalls.components.common.LocaleId +import com.revenuecat.purchases.paywalls.components.common.PaywallComponentsConfig +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.PaywallState import org.assertj.core.api.Assertions.assertThat import org.junit.Test +import java.net.URL import java.text.NumberFormat import java.util.Locale -class VariableContextTests { +class MostExpensivePricePerMonthMicrosTests { @Test fun `mostExpensivePricePerMonthMicros should be null for empty package list`() { @@ -19,7 +31,7 @@ class VariableContextTests { val packages = emptyList() // Act - val actual = VariableContext(packages).mostExpensivePricePerMonthMicros + val actual = PaywallState(packages).mostExpensivePricePerMonthMicros // Assert assertThat(actual).isNull() @@ -32,7 +44,7 @@ class VariableContextTests { val expected = 1_000_000L // Act - val actual = VariableContext(listOf(package1)).mostExpensivePricePerMonthMicros + val actual = PaywallState(listOf(package1)).mostExpensivePricePerMonthMicros // Assert assertThat(actual).isEqualTo(expected) @@ -47,7 +59,7 @@ class VariableContextTests { val expected = 2_000_000L // Act - val actual = VariableContext(listOf(package1, package2, package3)).mostExpensivePricePerMonthMicros + val actual = PaywallState(listOf(package1, package2, package3)).mostExpensivePricePerMonthMicros // Assert assertThat(actual).isEqualTo(expected) @@ -63,7 +75,7 @@ class VariableContextTests { val expected = weekly.product.pricePerMonth()?.amountMicros // Act - val actual = VariableContext(listOf(monthly, weekly, yearly)).mostExpensivePricePerMonthMicros + val actual = PaywallState(listOf(monthly, weekly, yearly)).mostExpensivePricePerMonthMicros // Assert assertThat(actual).isEqualTo(expected) @@ -77,7 +89,7 @@ class VariableContextTests { val expected = 1_000_000L // Act - val actual = VariableContext(listOf(package1, package2)).mostExpensivePricePerMonthMicros + val actual = PaywallState(listOf(package1, package2)).mostExpensivePricePerMonthMicros // Assert assertThat(actual).isEqualTo(expected) @@ -90,7 +102,7 @@ class VariableContextTests { val package2 = lifetimePackageWithPrice(2_000_000_000) // Act - val actual = VariableContext(listOf(package1, package2)).mostExpensivePricePerMonthMicros + val actual = PaywallState(listOf(package1, package2)).mostExpensivePricePerMonthMicros // Assert assertThat(actual).isNull() @@ -182,4 +194,30 @@ class VariableContextTests { */ private fun formatMicrosToCurrency(micros: Long, locale: Locale = Locale.US): String = NumberFormat.getCurrencyInstance(locale).format(micros / 1_000_000.0) + + @Suppress("TestFunctionName") + private fun PaywallState(packages: List): PaywallState.Loaded.Components { + val data = PaywallComponentsData( + templateName = "template", + assetBaseURL = URL("https://assets.pawwalls.com"), + componentsConfig = ComponentsConfig( + base = PaywallComponentsConfig( + stack = StackComponent(components = emptyList()), + background = Background.Color(ColorScheme(light = ColorInfo.Hex(Color.White.toArgb()))), + stickyFooter = null, + ), + ), + componentsLocalizations = emptyMap(), + defaultLocaleIdentifier = LocaleId("en_US"), + ) + val offering = Offering( + identifier = "identifier", + serverDescription = "serverDescription", + metadata = emptyMap(), + availablePackages = packages, + paywallComponents = data, + ) + + return PaywallState.Loaded.Components(offering, data) + } } From d7a13679e4c88bf47926b59da10ac5cd53418bee Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Mon, 16 Dec 2024 17:17:51 +0100 Subject: [PATCH 33/37] Moves MostExpensivePricePerMonthMicrosTests to the data package. --- .../state => data}/MostExpensivePricePerMonthMicrosTests.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) rename ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/{components/state => data}/MostExpensivePricePerMonthMicrosTests.kt (98%) diff --git a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/state/MostExpensivePricePerMonthMicrosTests.kt b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/MostExpensivePricePerMonthMicrosTests.kt similarity index 98% rename from ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/state/MostExpensivePricePerMonthMicrosTests.kt rename to ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/MostExpensivePricePerMonthMicrosTests.kt index e3ce942c7f..83c6057504 100644 --- a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/state/MostExpensivePricePerMonthMicrosTests.kt +++ b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/MostExpensivePricePerMonthMicrosTests.kt @@ -1,4 +1,4 @@ -package com.revenuecat.purchases.ui.revenuecatui.components.state +package com.revenuecat.purchases.ui.revenuecatui.data import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb @@ -16,7 +16,6 @@ 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.PaywallState import org.assertj.core.api.Assertions.assertThat import org.junit.Test import java.net.URL From dfc5024981642245af469a26aaf8f8c0beb73cfe Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Mon, 16 Dec 2024 18:18:01 +0100 Subject: [PATCH 34/37] TextComponentView uses PaywallState. --- .../revenuecatui/components/ComponentView.kt | 8 +- .../components/LoadedPaywallComponents.kt | 1 + .../components/button/ButtonComponentView.kt | 39 +++++++- .../components/stack/StackComponentView.kt | 41 +++++++- .../components/text/TextComponentView.kt | 96 +++++++++++-------- .../stack/StackComponentViewTests.kt | 10 +- .../components/text/TextComponentViewTests.kt | 24 +++-- .../revenuecatui/helpers/FakePaywallState.kt | 42 ++++++++ 8 files changed, 202 insertions(+), 59 deletions(-) create mode 100644 ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/helpers/FakePaywallState.kt diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/ComponentView.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/ComponentView.kt index 53f0a2d11b..21844795e6 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/ComponentView.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/ComponentView.kt @@ -13,6 +13,7 @@ import com.revenuecat.purchases.ui.revenuecatui.components.style.ImageComponentS import com.revenuecat.purchases.ui.revenuecatui.components.style.StackComponentStyle import com.revenuecat.purchases.ui.revenuecatui.components.style.TextComponentStyle import com.revenuecat.purchases.ui.revenuecatui.components.text.TextComponentView +import com.revenuecat.purchases.ui.revenuecatui.data.PaywallState /** * A Composable that can show any [ComponentStyle]. @@ -21,10 +22,11 @@ import com.revenuecat.purchases.ui.revenuecatui.components.text.TextComponentVie @Composable internal fun ComponentView( style: ComponentStyle, + state: PaywallState.Loaded.Components, modifier: Modifier = Modifier, ) = when (style) { - is StackComponentStyle -> StackComponentView(style = style, modifier = modifier) - is TextComponentStyle -> TextComponentView(style = style, modifier = modifier) + is StackComponentStyle -> StackComponentView(style = style, state = state, modifier = modifier) + is TextComponentStyle -> TextComponentView(style = style, state = state, modifier = modifier) is ImageComponentStyle -> ImageComponentView(style = style, modifier = modifier) - is ButtonComponentStyle -> ButtonComponentView(style = style, modifier = modifier) + is ButtonComponentStyle -> ButtonComponentView(style = style, state = state, modifier = modifier) } 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 a987d71a29..0b9ae5f578 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 @@ -76,6 +76,7 @@ internal fun LoadedPaywallComponents( ComponentView( style = style, + state = state, modifier = modifier .fillMaxSize() .background(background), diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/button/ButtonComponentView.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/button/ButtonComponentView.kt index 6dcf526754..ad847121f6 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/button/ButtonComponentView.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/button/ButtonComponentView.kt @@ -11,6 +11,13 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.revenuecat.purchases.Offering +import com.revenuecat.purchases.paywalls.components.StackComponent +import com.revenuecat.purchases.paywalls.components.common.Background +import com.revenuecat.purchases.paywalls.components.common.ComponentsConfig +import com.revenuecat.purchases.paywalls.components.common.LocaleId +import com.revenuecat.purchases.paywalls.components.common.PaywallComponentsConfig +import com.revenuecat.purchases.paywalls.components.common.PaywallComponentsData import com.revenuecat.purchases.paywalls.components.properties.Border import com.revenuecat.purchases.paywalls.components.properties.ColorInfo import com.revenuecat.purchases.paywalls.components.properties.ColorScheme @@ -32,14 +39,18 @@ import com.revenuecat.purchases.ui.revenuecatui.components.stack.StackComponentV import com.revenuecat.purchases.ui.revenuecatui.components.style.ButtonComponentStyle 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 java.net.URL @Composable internal fun ButtonComponentView( style: ButtonComponentStyle, + state: PaywallState.Loaded.Components, modifier: Modifier = Modifier, ) { StackComponentView( style.stackComponentStyle, + state, modifier.clickable { style.actionHandler(style.action) }, ) } @@ -47,7 +58,7 @@ internal fun ButtonComponentView( @Preview @Composable private fun ButtonComponentView_Preview_Default() { - ButtonComponentView(previewButtonComponentStyle()) + ButtonComponentView(previewButtonComponentStyle(), previewEmptyState()) } @Composable @@ -98,3 +109,29 @@ private fun previewButtonComponentStyle( actionHandler = actionHandler, ) } + +private fun previewEmptyState(): PaywallState.Loaded.Components { + val data = PaywallComponentsData( + templateName = "template", + assetBaseURL = URL("https://assets.pawwalls.com"), + componentsConfig = ComponentsConfig( + base = PaywallComponentsConfig( + // This would normally contain at least one ButtonComponent, but that's not needed for previews. + stack = StackComponent(components = emptyList()), + background = Background.Color(ColorScheme(light = ColorInfo.Hex(Color.White.toArgb()))), + stickyFooter = null, + ), + ), + componentsLocalizations = emptyMap(), + defaultLocaleIdentifier = LocaleId("en_US"), + ) + val offering = Offering( + identifier = "identifier", + serverDescription = "serverDescription", + metadata = emptyMap(), + availablePackages = emptyList(), + paywallComponents = data, + ) + + return PaywallState.Loaded.Components(offering, data) +} diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/stack/StackComponentView.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/stack/StackComponentView.kt index bdb906f386..6d51255e0d 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/stack/StackComponentView.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/stack/StackComponentView.kt @@ -17,6 +17,13 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.revenuecat.purchases.Offering +import com.revenuecat.purchases.paywalls.components.StackComponent +import com.revenuecat.purchases.paywalls.components.common.Background +import com.revenuecat.purchases.paywalls.components.common.ComponentsConfig +import com.revenuecat.purchases.paywalls.components.common.LocaleId +import com.revenuecat.purchases.paywalls.components.common.PaywallComponentsConfig +import com.revenuecat.purchases.paywalls.components.common.PaywallComponentsData import com.revenuecat.purchases.paywalls.components.properties.Border import com.revenuecat.purchases.paywalls.components.properties.ColorInfo import com.revenuecat.purchases.paywalls.components.properties.ColorScheme @@ -49,12 +56,15 @@ import com.revenuecat.purchases.ui.revenuecatui.components.properties.rememberCo import com.revenuecat.purchases.ui.revenuecatui.components.properties.rememberShadowStyle 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.extensions.applyIfNotNull +import java.net.URL @Suppress("LongMethod") @Composable internal fun StackComponentView( style: StackComponentStyle, + state: PaywallState.Loaded.Components, modifier: Modifier = Modifier, ) { if (style.visible) { @@ -74,7 +84,7 @@ internal fun StackComponentView( } val content: @Composable () -> Unit = remember(style.children) { - @Composable { style.children.forEach { child -> ComponentView(style = child) } } + @Composable { style.children.forEach { child -> ComponentView(style = child, state = state) } } } // Show the right container composable depending on the dimension. @@ -142,6 +152,7 @@ private fun StackComponentView_Preview_Vertical() { y = 3.0, ), ), + state = previewEmptyState(), ) } } @@ -175,6 +186,7 @@ private fun StackComponentView_Preview_Horizontal() { y = 5.0, ), ), + state = previewEmptyState(), ) } } @@ -247,6 +259,7 @@ private fun StackComponentView_Preview_ZLayer() { y = 5.0, ), ), + state = previewEmptyState(), ) } } @@ -290,3 +303,29 @@ private fun previewChildren() = listOf( margin = Padding(top = 0.0, bottom = 0.0, leading = 0.0, trailing = 0.0).toPaddingValues(), ), ) + +private fun previewEmptyState(): PaywallState.Loaded.Components { + val data = PaywallComponentsData( + templateName = "template", + assetBaseURL = URL("https://assets.pawwalls.com"), + componentsConfig = ComponentsConfig( + base = PaywallComponentsConfig( + // This would normally contain at least one StackComponent, but that's not needed for previews. + stack = StackComponent(components = emptyList()), + background = Background.Color(ColorScheme(light = ColorInfo.Hex(Color.White.toArgb()))), + stickyFooter = null, + ), + ), + componentsLocalizations = emptyMap(), + defaultLocaleIdentifier = LocaleId("en_US"), + ) + val offering = Offering( + identifier = "identifier", + serverDescription = "serverDescription", + metadata = emptyMap(), + availablePackages = emptyList(), + paywallComponents = data, + ) + + return PaywallState.Loaded.Components(offering, data) +} diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/text/TextComponentView.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/text/TextComponentView.kt index e2fafd456f..d5e53515fb 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/text/TextComponentView.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/text/TextComponentView.kt @@ -14,9 +14,15 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.revenuecat.purchases.Offering +import com.revenuecat.purchases.paywalls.components.StackComponent +import com.revenuecat.purchases.paywalls.components.common.Background +import com.revenuecat.purchases.paywalls.components.common.ComponentsConfig +import com.revenuecat.purchases.paywalls.components.common.LocaleId +import com.revenuecat.purchases.paywalls.components.common.PaywallComponentsConfig +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.paywalls.components.properties.FontSize @@ -38,25 +44,28 @@ import com.revenuecat.purchases.ui.revenuecatui.components.modifier.background import com.revenuecat.purchases.ui.revenuecatui.components.modifier.size import com.revenuecat.purchases.ui.revenuecatui.components.properties.ColorStyle import com.revenuecat.purchases.ui.revenuecatui.components.properties.rememberColorStyle -import com.revenuecat.purchases.ui.revenuecatui.components.state.PackageContext import com.revenuecat.purchases.ui.revenuecatui.components.style.TextComponentStyle import com.revenuecat.purchases.ui.revenuecatui.composables.Markdown +import com.revenuecat.purchases.ui.revenuecatui.data.PaywallState import com.revenuecat.purchases.ui.revenuecatui.data.processed.VariableDataProvider import com.revenuecat.purchases.ui.revenuecatui.data.processed.VariableProcessor import com.revenuecat.purchases.ui.revenuecatui.extensions.applyIfNotNull import com.revenuecat.purchases.ui.revenuecatui.helpers.toResourceProvider +import java.net.URL @Composable internal fun TextComponentView( style: TextComponentStyle, + state: PaywallState.Loaded.Components, modifier: Modifier = Modifier, - // TODO Remove these default values - packageContext: PackageContext = PackageContext(null, PackageContext.VariableContext(emptyList())), - locale: Locale = Locale.current, ) { val context = LocalContext.current val variableDataProvider = remember { VariableDataProvider(context.toResourceProvider()) } - val text = rememberProcessedText(style.text, packageContext, variableDataProvider, locale) + val text = rememberProcessedText( + originalText = style.text, + variables = variableDataProvider, + state = state, + ) val colorStyle = rememberColorStyle(scheme = style.color) val backgroundColorStyle = style.backgroundColor?.let { rememberColorStyle(scheme = it) } @@ -96,27 +105,26 @@ internal fun TextComponentView( @Composable private fun rememberProcessedText( originalText: String, - packageContext: PackageContext, variables: VariableDataProvider, - locale: Locale, + state: PaywallState.Loaded.Components, ): String { - val processedText by remember(packageContext, variables, locale) { + val processedText by remember(originalText) { derivedStateOf { - packageContext.selectedPackage?.let { selectedPackage -> + state.selectedPackage?.let { selectedPackage -> val discount = discountPercentage( pricePerMonthMicros = selectedPackage.product.pricePerMonth()?.amountMicros, - mostExpensiveMicros = packageContext.variableContext.mostExpensivePricePerMonthMicros, + mostExpensiveMicros = state.mostExpensivePricePerMonthMicros, ) val variableContext: VariableProcessor.PackageContext = VariableProcessor.PackageContext( discountRelativeToMostExpensivePerMonth = discount, - showZeroDecimalPlacePrices = packageContext.variableContext.showZeroDecimalPlacePrices, + showZeroDecimalPlacePrices = state.showZeroDecimalPlacePrices, ) VariableProcessor.processVariables( variableDataProvider = variables, context = variableContext, originalString = originalText, rcPackage = selectedPackage, - locale = java.util.Locale.forLanguageTag(locale.toLanguageTag()), + locale = java.util.Locale.forLanguageTag(state.locale.toLanguageTag()), ) } ?: originalText } @@ -144,8 +152,7 @@ private fun TextComponentView_Preview_Default() { text = "Hello, world", color = ColorScheme(light = ColorInfo.Hex(Color.Black.toArgb())), ), - packageContext = previewPackageState(), - locale = Locale.current, + state = previewEmptyState(), ) } @@ -159,8 +166,7 @@ private fun TextComponentView_Preview_SerifFont() { fontFamily = "serif", size = Size(width = Fit, height = Fit), ), - packageContext = previewPackageState(), - locale = Locale.current, + state = previewEmptyState(), ) } @@ -174,8 +180,7 @@ private fun TextComponentView_Preview_SansSerifFont() { fontFamily = "sans-serif", size = Size(width = Fit, height = Fit), ), - packageContext = previewPackageState(), - locale = Locale.current, + state = previewEmptyState(), ) } @@ -189,8 +194,7 @@ private fun TextComponentView_Preview_MonospaceFont() { fontFamily = "monospace", size = Size(width = Fit, height = Fit), ), - packageContext = previewPackageState(), - locale = Locale.current, + state = previewEmptyState(), ) } @@ -204,8 +208,7 @@ private fun TextComponentView_Preview_CursiveFont() { fontFamily = "cursive", size = Size(width = Fit, height = Fit), ), - packageContext = previewPackageState(), - locale = Locale.current, + state = previewEmptyState(), ) } @@ -219,8 +222,7 @@ private fun TextComponentView_Preview_FontSize() { fontSize = FontSize.HEADING_L, size = Size(width = Fit, height = Fit), ), - packageContext = previewPackageState(), - locale = Locale.current, + state = previewEmptyState(), ) } @@ -234,10 +236,9 @@ private fun TextComponentView_Preview_HorizontalAlignment() { size = Size(width = Fit, height = Fit), horizontalAlignment = HorizontalAlignment.TRAILING, ), - packageContext = previewPackageState(), + state = previewEmptyState(), // Our width is Fit, but we are forced to be wider than our contents. modifier = Modifier.widthIn(min = 400.dp), - locale = Locale.current, ) } @@ -256,8 +257,7 @@ private fun TextComponentView_Preview_Customizations() { padding = Padding(top = 10.0, bottom = 10.0, leading = 20.0, trailing = 20.0), margin = Padding(top = 20.0, bottom = 20.0, leading = 10.0, trailing = 10.0), ), - packageContext = previewPackageState(), - locale = Locale.current, + state = previewEmptyState(), ) } @@ -270,8 +270,7 @@ private fun TextComponentView_Preview_Markdown() { "Click [here](https://revenuecat.com)", color = ColorScheme(light = ColorInfo.Hex(Color.Black.toArgb())), ), - packageContext = previewPackageState(), - locale = Locale.current, + state = previewEmptyState(), ) } @@ -309,8 +308,7 @@ private fun TextComponentView_Preview_LinearGradient() { padding = Padding(top = 10.0, bottom = 10.0, leading = 20.0, trailing = 20.0), margin = Padding(top = 20.0, bottom = 20.0, leading = 10.0, trailing = 10.0), ), - packageContext = previewPackageState(), - locale = Locale.current, + state = previewEmptyState(), ) } @@ -347,8 +345,7 @@ private fun TextComponentView_Preview_RadialGradient() { padding = Padding(top = 10.0, bottom = 10.0, leading = 20.0, trailing = 20.0), margin = Padding(top = 20.0, bottom = 20.0, leading = 10.0, trailing = 10.0), ), - packageContext = previewPackageState(), - locale = Locale.current, + state = previewEmptyState(), ) } @@ -385,11 +382,28 @@ private fun previewTextComponentStyle( ) } -private fun previewPackageState(): PackageContext = - PackageContext( - initialSelectedPackage = null, - initialVariableContext = PackageContext.VariableContext( - packages = emptyList(), - showZeroDecimalPlacePrices = true, +private fun previewEmptyState(): PaywallState.Loaded.Components { + val data = PaywallComponentsData( + templateName = "template", + assetBaseURL = URL("https://assets.pawwalls.com"), + componentsConfig = ComponentsConfig( + base = PaywallComponentsConfig( + // This would normally contain at least one TextComponent, but that's not needed for previews. + stack = StackComponent(components = emptyList()), + background = Background.Color(ColorScheme(light = ColorInfo.Hex(Color.White.toArgb()))), + stickyFooter = null, + ), ), + componentsLocalizations = emptyMap(), + defaultLocaleIdentifier = LocaleId("en_US"), + ) + val offering = Offering( + identifier = "identifier", + serverDescription = "serverDescription", + metadata = emptyMap(), + availablePackages = emptyList(), + paywallComponents = data, ) + + return PaywallState.Loaded.Components(offering, data) +} 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 f27f9fb2a9..3b1d7a3299 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 @@ -29,6 +29,7 @@ import com.revenuecat.purchases.ui.revenuecatui.components.ComponentViewState import com.revenuecat.purchases.ui.revenuecatui.components.ScreenCondition import com.revenuecat.purchases.ui.revenuecatui.components.style.StackComponentStyle import com.revenuecat.purchases.ui.revenuecatui.components.style.StyleFactory +import com.revenuecat.purchases.ui.revenuecatui.helpers.FakePaywallState import com.revenuecat.purchases.ui.revenuecatui.helpers.getOrThrow import com.revenuecat.purchases.ui.revenuecatui.helpers.themeChangingTest import org.junit.Before @@ -72,13 +73,14 @@ class StackComponentViewTests { dark = ColorInfo.Hex(expectedDarkColor.toArgb()), ), ) + val state = FakePaywallState(component) themeChangingTest( arrange = { // We don't want to recreate the entire tree every time the theme, or any other state, changes. styleFactory.create(component).getOrThrow() as StackComponentStyle }, - act = { StackComponentView(style = it, modifier = Modifier.testTag("stack")) }, + act = { StackComponentView(style = it, state = state, modifier = Modifier.testTag("stack")) }, assert = { theme -> theme.setLight() onNodeWithTag("stack") @@ -115,6 +117,7 @@ class StackComponentViewTests { width = borderWidthDp ), ) + val state = FakePaywallState(component) themeChangingTest( arrange = { @@ -124,7 +127,7 @@ class StackComponentViewTests { // We don't want to recreate the entire tree every time the theme, or any other state, changes. styleFactory.create(component).getOrThrow() as StackComponentStyle }, - act = { StackComponentView(style = it, modifier = Modifier.testTag("stack")) }, + act = { StackComponentView(style = it, state = state, modifier = Modifier.testTag("stack")) }, assert = { theme -> theme.setLight() onNodeWithTag("stack") @@ -171,6 +174,7 @@ class StackComponentViewTests { ), backgroundColor = ColorScheme(light = ColorInfo.Hex(expectedBackgroundColor.toArgb())), ) + val state = FakePaywallState(component) themeChangingTest( arrange = { @@ -186,7 +190,7 @@ class StackComponentViewTests { .background(expectedBackgroundColor), contentAlignment = Alignment.Center, ) { - StackComponentView(style = it, modifier = Modifier.testTag("stack")) + StackComponentView(style = it, state = state, modifier = Modifier.testTag("stack")) } }, assert = { theme -> 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 dbde2f4d7c..95246e5a14 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 @@ -27,6 +27,7 @@ import com.revenuecat.purchases.ui.revenuecatui.components.ComponentViewState import com.revenuecat.purchases.ui.revenuecatui.components.ScreenCondition import com.revenuecat.purchases.ui.revenuecatui.components.style.StyleFactory import com.revenuecat.purchases.ui.revenuecatui.components.style.TextComponentStyle +import com.revenuecat.purchases.ui.revenuecatui.helpers.FakePaywallState import com.revenuecat.purchases.ui.revenuecatui.helpers.getOrThrow import com.revenuecat.purchases.ui.revenuecatui.helpers.themeChangingTest import org.assertj.core.api.Assertions.assertThat @@ -71,13 +72,14 @@ class TextComponentViewTests { dark = ColorInfo.Hex(expectedDarkColor.toArgb()), ), ) + val state = FakePaywallState(component) themeChangingTest( arrange = { // We don't want to recreate the entire tree every time the theme, or any other state, changes. styleFactory.create(component).getOrThrow() as TextComponentStyle }, - act = { TextComponentView(style = it) }, + act = { TextComponentView(style = it, state = state) }, assert = { theme -> theme.setLight() onNodeWithText(localizationDictionary.values.first().value) @@ -111,13 +113,14 @@ class TextComponentViewTests { dark = ColorInfo.Hex(expectedDarkColor.toArgb()), ), ) + val state = FakePaywallState(component) themeChangingTest( arrange = { // We don't want to recreate the entire tree every time the theme, or any other state, changes. styleFactory.create(component).getOrThrow() as TextComponentStyle }, - act = { TextComponentView(style = it) }, + act = { TextComponentView(style = it, state = state) }, assert = { theme -> theme.setLight() onNodeWithText(localizationDictionary.values.first().value) @@ -145,19 +148,18 @@ class TextComponentViewTests { val textId = localizationDictionary.keys.first() val color = ColorScheme(light = ColorInfo.Hex(Color.Black.toArgb())) val size = Size(Fit, Fit) + val largeTextComponent = TextComponent(text = textId, color = color, fontSize = FontSize.HEADING_L, size = size) + val smallTextComponent = TextComponent(text = textId, color = color, fontSize = FontSize.BODY_S, size = size) + val state = FakePaywallState(largeTextComponent, smallTextComponent) setContent { - val largeTextStyle = styleFactory.create( - TextComponent(text = textId, color = color, fontSize = FontSize.HEADING_L, size = size) - ).getOrThrow() as TextComponentStyle - val smallTextStyle = styleFactory.create( - TextComponent(text = textId, color = color, fontSize = FontSize.BODY_S, size = size) - ).getOrThrow() as TextComponentStyle + val largeTextStyle = styleFactory.create(largeTextComponent).getOrThrow() as TextComponentStyle + val smallTextStyle = styleFactory.create(smallTextComponent).getOrThrow() as TextComponentStyle // Act MaterialTheme { Column(modifier = Modifier.fillMaxSize()) { - TextComponentView(style = largeTextStyle, modifier = Modifier.testTag("large")) - TextComponentView(style = smallTextStyle, modifier = Modifier.testTag("small")) + TextComponentView(style = largeTextStyle, state = state, modifier = Modifier.testTag("large")) + TextComponentView(style = smallTextStyle, state = state, modifier = Modifier.testTag("small")) } } } @@ -186,4 +188,6 @@ class TextComponentViewTests { */ private fun SemanticsNodeInteraction.assertBackgroundColorEquals(color: Color): SemanticsNodeInteraction = assertPixelColorEquals(startX = 0, startY = 0, width = 4, height = 4, color = color) + + } 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 new file mode 100644 index 0000000000..de1a00f9da --- /dev/null +++ b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/helpers/FakePaywallState.kt @@ -0,0 +1,42 @@ +package com.revenuecat.purchases.ui.revenuecatui.helpers + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import com.revenuecat.purchases.Offering +import com.revenuecat.purchases.paywalls.components.PaywallComponent +import com.revenuecat.purchases.paywalls.components.StackComponent +import com.revenuecat.purchases.paywalls.components.common.Background +import com.revenuecat.purchases.paywalls.components.common.ComponentsConfig +import com.revenuecat.purchases.paywalls.components.common.LocaleId +import com.revenuecat.purchases.paywalls.components.common.PaywallComponentsConfig +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.PaywallState +import java.net.URL + +@Suppress("TestFunctionName") +internal fun FakePaywallState(vararg component: PaywallComponent): PaywallState.Loaded.Components { + val data = PaywallComponentsData( + templateName = "template", + assetBaseURL = URL("https://assets.pawwalls.com"), + componentsConfig = ComponentsConfig( + base = PaywallComponentsConfig( + stack = StackComponent(components = component.toList()), + background = Background.Color(ColorScheme(light = ColorInfo.Hex(Color.White.toArgb()))), + stickyFooter = null, + ), + ), + componentsLocalizations = mapOf(LocaleId("en_US") to emptyMap()), + defaultLocaleIdentifier = LocaleId("en_US"), + ) + val offering = Offering( + identifier = "identifier", + serverDescription = "serverDescription", + metadata = emptyMap(), + availablePackages = emptyList(), + paywallComponents = data, + ) + + return PaywallState.Loaded.Components(offering, data) +} From f69c7b968edcb45481ac9f1eee8705670e0598ca Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Mon, 16 Dec 2024 18:28:34 +0100 Subject: [PATCH 35/37] MostExpensivePricePerMonthMicrosTests uses FakePaywallState. --- .../MostExpensivePricePerMonthMicrosTests.kt | 51 +++---------------- .../revenuecatui/helpers/FakePaywallState.kt | 20 ++++++-- 2 files changed, 23 insertions(+), 48 deletions(-) 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 83c6057504..c0ec9b7cad 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 @@ -1,24 +1,13 @@ package com.revenuecat.purchases.ui.revenuecatui.data -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb -import com.revenuecat.purchases.Offering import com.revenuecat.purchases.Package 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.StackComponent -import com.revenuecat.purchases.paywalls.components.common.Background -import com.revenuecat.purchases.paywalls.components.common.ComponentsConfig -import com.revenuecat.purchases.paywalls.components.common.LocaleId -import com.revenuecat.purchases.paywalls.components.common.PaywallComponentsConfig -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.helpers.FakePaywallState import org.assertj.core.api.Assertions.assertThat import org.junit.Test -import java.net.URL import java.text.NumberFormat import java.util.Locale @@ -30,7 +19,7 @@ class MostExpensivePricePerMonthMicrosTests { val packages = emptyList() // Act - val actual = PaywallState(packages).mostExpensivePricePerMonthMicros + val actual = FakePaywallState(packages = packages).mostExpensivePricePerMonthMicros // Assert assertThat(actual).isNull() @@ -43,7 +32,7 @@ class MostExpensivePricePerMonthMicrosTests { val expected = 1_000_000L // Act - val actual = PaywallState(listOf(package1)).mostExpensivePricePerMonthMicros + val actual = FakePaywallState(package1).mostExpensivePricePerMonthMicros // Assert assertThat(actual).isEqualTo(expected) @@ -58,7 +47,7 @@ class MostExpensivePricePerMonthMicrosTests { val expected = 2_000_000L // Act - val actual = PaywallState(listOf(package1, package2, package3)).mostExpensivePricePerMonthMicros + val actual = FakePaywallState(package1, package2, package3).mostExpensivePricePerMonthMicros // Assert assertThat(actual).isEqualTo(expected) @@ -74,7 +63,7 @@ class MostExpensivePricePerMonthMicrosTests { val expected = weekly.product.pricePerMonth()?.amountMicros // Act - val actual = PaywallState(listOf(monthly, weekly, yearly)).mostExpensivePricePerMonthMicros + val actual = FakePaywallState(monthly, weekly, yearly).mostExpensivePricePerMonthMicros // Assert assertThat(actual).isEqualTo(expected) @@ -88,7 +77,7 @@ class MostExpensivePricePerMonthMicrosTests { val expected = 1_000_000L // Act - val actual = PaywallState(listOf(package1, package2)).mostExpensivePricePerMonthMicros + val actual = FakePaywallState(package1, package2).mostExpensivePricePerMonthMicros // Assert assertThat(actual).isEqualTo(expected) @@ -101,7 +90,7 @@ class MostExpensivePricePerMonthMicrosTests { val package2 = lifetimePackageWithPrice(2_000_000_000) // Act - val actual = PaywallState(listOf(package1, package2)).mostExpensivePricePerMonthMicros + val actual = FakePaywallState(package1, package2).mostExpensivePricePerMonthMicros // Assert assertThat(actual).isNull() @@ -193,30 +182,4 @@ class MostExpensivePricePerMonthMicrosTests { */ private fun formatMicrosToCurrency(micros: Long, locale: Locale = Locale.US): String = NumberFormat.getCurrencyInstance(locale).format(micros / 1_000_000.0) - - @Suppress("TestFunctionName") - private fun PaywallState(packages: List): PaywallState.Loaded.Components { - val data = PaywallComponentsData( - templateName = "template", - assetBaseURL = URL("https://assets.pawwalls.com"), - componentsConfig = ComponentsConfig( - base = PaywallComponentsConfig( - stack = StackComponent(components = emptyList()), - background = Background.Color(ColorScheme(light = ColorInfo.Hex(Color.White.toArgb()))), - stickyFooter = null, - ), - ), - componentsLocalizations = emptyMap(), - defaultLocaleIdentifier = LocaleId("en_US"), - ) - val offering = Offering( - identifier = "identifier", - serverDescription = "serverDescription", - metadata = emptyMap(), - availablePackages = packages, - paywallComponents = data, - ) - - return PaywallState.Loaded.Components(offering, data) - } } 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 de1a00f9da..9cb82bead3 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 @@ -1,8 +1,11 @@ +@file:Suppress("TestFunctionName") + package com.revenuecat.purchases.ui.revenuecatui.helpers import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import com.revenuecat.purchases.Offering +import com.revenuecat.purchases.Package import com.revenuecat.purchases.paywalls.components.PaywallComponent import com.revenuecat.purchases.paywalls.components.StackComponent import com.revenuecat.purchases.paywalls.components.common.Background @@ -15,14 +18,23 @@ import com.revenuecat.purchases.paywalls.components.properties.ColorScheme import com.revenuecat.purchases.ui.revenuecatui.data.PaywallState import java.net.URL -@Suppress("TestFunctionName") -internal fun FakePaywallState(vararg component: PaywallComponent): PaywallState.Loaded.Components { + +internal fun FakePaywallState(vararg pkg: Package): PaywallState.Loaded.Components = + FakePaywallState(packages = pkg.toList()) + +internal fun FakePaywallState(vararg component: PaywallComponent): PaywallState.Loaded.Components = + FakePaywallState(components = component.toList()) + +internal fun FakePaywallState( + components: List = emptyList(), + packages: List = emptyList(), +): PaywallState.Loaded.Components { val data = PaywallComponentsData( templateName = "template", assetBaseURL = URL("https://assets.pawwalls.com"), componentsConfig = ComponentsConfig( base = PaywallComponentsConfig( - stack = StackComponent(components = component.toList()), + stack = StackComponent(components = components), background = Background.Color(ColorScheme(light = ColorInfo.Hex(Color.White.toArgb()))), stickyFooter = null, ), @@ -34,7 +46,7 @@ internal fun FakePaywallState(vararg component: PaywallComponent): PaywallState. identifier = "identifier", serverDescription = "serverDescription", metadata = emptyMap(), - availablePackages = emptyList(), + availablePackages = packages, paywallComponents = data, ) From 55445180b0b892b2fb9ed69a01bb87e6d0d1c318 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Tue, 17 Dec 2024 11:18:14 +0100 Subject: [PATCH 36/37] Reorders imports in ButtonComponentView. --- .../ui/revenuecatui/components/button/ButtonComponentView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/button/ButtonComponentView.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/button/ButtonComponentView.kt index ead5a23a5d..eb334b0984 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/button/ButtonComponentView.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/button/ButtonComponentView.kt @@ -45,8 +45,8 @@ import com.revenuecat.purchases.ui.revenuecatui.components.style.ButtonComponent 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 java.net.URL import kotlinx.coroutines.launch +import java.net.URL @Composable internal fun ButtonComponentView( From fb75780c27dc729a05b941a979d2b65349c2d5a7 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Tue, 17 Dec 2024 14:01:30 +0100 Subject: [PATCH 37/37] Fixes ButtonComponentViewTests. --- .../button/ButtonComponentViewTests.kt | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) 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 e9055e3cd8..82f25fc2c7 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 @@ -9,26 +9,28 @@ import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.revenuecat.purchases.paywalls.components.properties.Border import com.revenuecat.purchases.paywalls.components.properties.ColorInfo import com.revenuecat.purchases.paywalls.components.properties.ColorScheme import com.revenuecat.purchases.paywalls.components.properties.Dimension import com.revenuecat.purchases.paywalls.components.properties.FlexDistribution.START import com.revenuecat.purchases.paywalls.components.properties.FontSize import com.revenuecat.purchases.paywalls.components.properties.FontWeight -import com.revenuecat.purchases.paywalls.components.properties.HorizontalAlignment +import com.revenuecat.purchases.paywalls.components.properties.HorizontalAlignment.CENTER import com.revenuecat.purchases.paywalls.components.properties.Padding +import com.revenuecat.purchases.paywalls.components.properties.Shadow import com.revenuecat.purchases.paywalls.components.properties.Size import com.revenuecat.purchases.paywalls.components.properties.SizeConstraint.Fill import com.revenuecat.purchases.ui.revenuecatui.components.PaywallAction -import com.revenuecat.purchases.ui.revenuecatui.components.properties.BackgroundStyle -import com.revenuecat.purchases.ui.revenuecatui.components.properties.BorderStyle -import com.revenuecat.purchases.ui.revenuecatui.components.properties.ColorStyle.Solid -import com.revenuecat.purchases.ui.revenuecatui.components.properties.ShadowStyle +import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toAlignment +import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toFontWeight +import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toPaddingValues +import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toTextAlign import com.revenuecat.purchases.ui.revenuecatui.components.style.ButtonComponentStyle import com.revenuecat.purchases.ui.revenuecatui.components.style.StackComponentStyle import com.revenuecat.purchases.ui.revenuecatui.components.style.TextComponentStyle +import com.revenuecat.purchases.ui.revenuecatui.helpers.FakePaywallState import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.delay import org.assertj.core.api.Assertions.assertThat import org.junit.Rule import org.junit.Test @@ -57,27 +59,33 @@ class ButtonComponentViewTests { light = ColorInfo.Hex(Color.Black.toArgb()), ), fontSize = FontSize.BODY_M, - fontWeight = FontWeight.REGULAR, + fontWeight = FontWeight.REGULAR.toFontWeight(), fontFamily = null, - textAlign = HorizontalAlignment.CENTER, - horizontalAlignment = HorizontalAlignment.CENTER, + textAlign = CENTER.toTextAlign(), + horizontalAlignment = CENTER.toAlignment(), backgroundColor = ColorScheme( light = ColorInfo.Hex(Color.Yellow.toArgb()), ), size = Size(width = Fill, height = Fill), - padding = Padding(top = 8.0, bottom = 8.0, leading = 8.0, trailing = 8.0), - margin = Padding(top = 0.0, bottom = 24.0, leading = 0.0, trailing = 24.0), + padding = Padding(top = 8.0, bottom = 8.0, leading = 8.0, trailing = 8.0).toPaddingValues(), + margin = Padding(top = 0.0, bottom = 24.0, leading = 0.0, trailing = 24.0) + .toPaddingValues(), ), ), - dimension = Dimension.Vertical(alignment = HorizontalAlignment.CENTER, distribution = START), + dimension = Dimension.Vertical(alignment = CENTER, distribution = START), size = Size(width = Fill, height = Fill), spacing = 16.dp, - background = BackgroundStyle.Color(Solid(Color.Red)), + backgroundColor = ColorScheme(ColorInfo.Hex(Color.Red.toArgb())), padding = PaddingValues(all = 16.dp), margin = PaddingValues(all = 16.dp), shape = RoundedCornerShape(size = 20.dp), - border = BorderStyle(width = 2.dp, color = Solid(Color.Blue)), - shadow = ShadowStyle(color = Solid(Color.Black), radius = 10.dp, x = 0.dp, y = 3.dp), + border = Border(width = 2.0, color = ColorScheme(ColorInfo.Hex(Color.Blue.toArgb()))), + shadow = Shadow( + color = ColorScheme(ColorInfo.Hex(Color.Black.toArgb())), + radius = 10.0, + x = 0.0, + y = 3.0 + ), ), action = PaywallAction.PurchasePackage, actionHandler = { @@ -85,7 +93,7 @@ class ButtonComponentViewTests { completable.await() } ) - ButtonComponentView(style = style) + ButtonComponentView(style = style, state = FakePaywallState()) } val purchaseButton = composeTestRule.onNodeWithText("Purchase")