From ddfdb10ec9a28948dbe49cf97b8fbafdc9b21fe6 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Fri, 17 Jan 2025 17:41:57 +0100 Subject: [PATCH 01/16] Adds deserialization of UiConfig. --- .../com/revenuecat/purchases/Offerings.kt | 5 +- .../com/revenuecat/purchases/UiConfig.kt | 79 ++++++++ .../purchases/common/OfferingParser.kt | 15 ++ .../components/common/Localization.kt | 132 +++++++++++++ .../com/revenuecat/purchases/OfferingsTest.kt | 137 ++++++++++++++ .../com/revenuecat/purchases/UiConfigTests.kt | 177 ++++++++++++++++++ .../common/VariableLocalizationKeyTests.kt | 75 ++++++++ 7 files changed, 619 insertions(+), 1 deletion(-) create mode 100644 purchases/src/main/kotlin/com/revenuecat/purchases/UiConfig.kt create mode 100644 purchases/src/test/java/com/revenuecat/purchases/UiConfigTests.kt create mode 100644 purchases/src/test/java/com/revenuecat/purchases/paywalls/components/common/VariableLocalizationKeyTests.kt diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/Offerings.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/Offerings.kt index ff55e27c38..baad605f16 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/Offerings.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/Offerings.kt @@ -6,11 +6,14 @@ package com.revenuecat.purchases * @property current Current offering configured in the RevenueCat dashboard. * @property all Dictionary of all Offerings [Offering] objects keyed by their identifier. */ -data class Offerings internal constructor( +data class Offerings +@OptIn(InternalRevenueCatAPI::class) +internal constructor( val current: Offering?, val all: Map, internal val placements: Placements? = null, internal val targeting: Targeting? = null, + internal val uiConfig: UiConfig? = null, ) { constructor(current: Offering?, all: Map) : this(current, all, null, null) diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/UiConfig.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/UiConfig.kt new file mode 100644 index 0000000000..87dfc70fcd --- /dev/null +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/UiConfig.kt @@ -0,0 +1,79 @@ +package com.revenuecat.purchases + +import com.revenuecat.purchases.paywalls.components.common.LocaleId +import com.revenuecat.purchases.paywalls.components.common.VariableLocalizationKey +import com.revenuecat.purchases.paywalls.components.properties.ColorScheme +import dev.drewhamilton.poko.Poko +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@InternalRevenueCatAPI +@Serializable +@JvmInline +value class ColorAlias(@get:JvmSynthetic val value: String) + +@InternalRevenueCatAPI +@Serializable +@JvmInline +value class FontAlias(@get:JvmSynthetic val value: String) + +@InternalRevenueCatAPI +@Poko +@Serializable +class UiConfig( + @get:JvmSynthetic + val app: AppConfig, + @get:JvmSynthetic + val localizations: Map> = emptyMap(), + @SerialName("variable_config") + @get:JvmSynthetic + val variableConfig: VariableConfig, +) { + + @InternalRevenueCatAPI + @Poko + @Serializable + class AppConfig( + @get:JvmSynthetic + val colors: Map = emptyMap(), + @get:JvmSynthetic + val fonts: Map = emptyMap(), + ) { + @InternalRevenueCatAPI + @Poko + @Serializable + class FontsConfig( + @get:JvmSynthetic + val android: FontInfo, + ) { + + @InternalRevenueCatAPI + @Serializable + sealed interface FontInfo { + @InternalRevenueCatAPI + @Poko + @Serializable + @SerialName("name") + class Name(@get:JvmSynthetic val value: String) : FontInfo + + @InternalRevenueCatAPI + @Poko + @Serializable + @SerialName("google_fonts") + class GoogleFonts(@get:JvmSynthetic val value: String) : FontInfo + } + } + } + + @InternalRevenueCatAPI + @Poko + @Serializable + class VariableConfig( + @SerialName("variable_compatibility_map") + @get:JvmSynthetic + val variableCompatibilityMap: Map = emptyMap(), + @SerialName("function_compatibility_map") + @get:JvmSynthetic + val functionCompatibilityMap: Map = emptyMap(), + ) +} 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 6c7d0e99f6..0a2de8a9db 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/common/OfferingParser.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/common/OfferingParser.kt @@ -7,6 +7,7 @@ import com.revenuecat.purchases.Offerings import com.revenuecat.purchases.Package import com.revenuecat.purchases.PackageType import com.revenuecat.purchases.PresentedOfferingContext +import com.revenuecat.purchases.UiConfig import com.revenuecat.purchases.api.BuildConfig import com.revenuecat.purchases.models.StoreProduct import com.revenuecat.purchases.paywalls.PaywallData @@ -38,6 +39,7 @@ internal abstract class OfferingParser { /** * Note: this may return an empty Offerings. */ + @OptIn(InternalRevenueCatAPI::class) fun createOfferings(offeringsJson: JSONObject, productsById: Map>): Offerings { log(LogIntent.DEBUG, OfferingStrings.BUILDING_OFFERINGS.format(productsById.size)) @@ -84,11 +86,24 @@ internal abstract class OfferingParser { } } + val uiConfigJson = offeringsJson.optJSONObject("ui_config") + + @Suppress("TooGenericExceptionCaught") + val uiConfig: UiConfig? = uiConfigJson?.let { + try { + json.decodeFromString(it.toString()) + } catch (e: Throwable) { + errorLog("Error deserializing ui_config", e) + null + } + } + return Offerings( current = offerings[currentOfferingID]?.withPresentedContext(null, targeting), all = offerings, placements = placements, targeting = targeting, + uiConfig = uiConfig, ) } 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 b9b5b1745d..2711d08cb5 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 @@ -5,6 +5,7 @@ import com.revenuecat.purchases.paywalls.components.properties.ThemeImageUrls import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.SerializationException import kotlinx.serialization.descriptors.PolymorphicKind @@ -64,3 +65,134 @@ private object LocalizationDataSerializer : KSerializer { decoder.decodeSerializableValue(LocalizationData.Image.serializer()) } } + +/** + * Keys for localized strings used as variable values. + */ +@InternalRevenueCatAPI +@Serializable +enum class VariableLocalizationKey { + + @SerialName("day") + DAY, + + @SerialName("daily") + DAILY, + + @SerialName("day_short") + DAY_SHORT, + + @SerialName("week") + WEEK, + + @SerialName("weekly") + WEEKLY, + + @SerialName("week_short") + WEEK_SHORT, + + @SerialName("month") + MONTH, + + @SerialName("monthly") + MONTHLY, + + @SerialName("month_short") + MONTH_SHORT, + + @SerialName("year") + YEAR, + + @SerialName("yearly") + YEARLY, + + @SerialName("year_short") + YEAR_SHORT, + + @SerialName("annual") + ANNUAL, + + @SerialName("annually") + ANNUALLY, + + @SerialName("annual_short") + ANNUAL_SHORT, + + @SerialName("free_price") + FREE_PRICE, + + @SerialName("percent") + PERCENT, + + @SerialName("num_day_zero") + NUM_DAY_ZERO, + + @SerialName("num_day_one") + NUM_DAY_ONE, + + @SerialName("num_day_two") + NUM_DAY_TWO, + + @SerialName("num_day_few") + NUM_DAY_FEW, + + @SerialName("num_day_many") + NUM_DAY_MANY, + + @SerialName("num_day_other") + NUM_DAY_OTHER, + + @SerialName("num_week_zero") + NUM_WEEK_ZERO, + + @SerialName("num_week_one") + NUM_WEEK_ONE, + + @SerialName("num_week_two") + NUM_WEEK_TWO, + + @SerialName("num_week_few") + NUM_WEEK_FEW, + + @SerialName("num_week_many") + NUM_WEEK_MANY, + + @SerialName("num_week_other") + NUM_WEEK_OTHER, + + @SerialName("num_month_zero") + NUM_MONTH_ZERO, + + @SerialName("num_month_one") + NUM_MONTH_ONE, + + @SerialName("num_month_two") + NUM_MONTH_TWO, + + @SerialName("num_month_few") + NUM_MONTH_FEW, + + @SerialName("num_month_many") + NUM_MONTH_MANY, + + @SerialName("num_month_other") + NUM_MONTH_OTHER, + + @SerialName("num_year_zero") + NUM_YEAR_ZERO, + + @SerialName("num_year_one") + NUM_YEAR_ONE, + + @SerialName("num_year_two") + NUM_YEAR_TWO, + + @SerialName("num_year_few") + NUM_YEAR_FEW, + + @SerialName("num_year_many") + NUM_YEAR_MANY, + + @SerialName("num_year_other") + NUM_YEAR_OTHER, +} diff --git a/purchases/src/test/java/com/revenuecat/purchases/OfferingsTest.kt b/purchases/src/test/java/com/revenuecat/purchases/OfferingsTest.kt index 5bd4298f4c..5f870210f5 100644 --- a/purchases/src/test/java/com/revenuecat/purchases/OfferingsTest.kt +++ b/purchases/src/test/java/com/revenuecat/purchases/OfferingsTest.kt @@ -7,8 +7,13 @@ package com.revenuecat.purchases import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.billingclient.api.ProductDetails +import com.revenuecat.purchases.UiConfig.AppConfig.FontsConfig.FontInfo import com.revenuecat.purchases.models.Period import com.revenuecat.purchases.models.StoreProduct +import com.revenuecat.purchases.paywalls.components.common.LocaleId +import com.revenuecat.purchases.paywalls.components.common.VariableLocalizationKey +import com.revenuecat.purchases.paywalls.components.properties.ColorInfo +import com.revenuecat.purchases.paywalls.parseRGBAColor import com.revenuecat.purchases.utils.getLifetimePackageJSON import com.revenuecat.purchases.utils.stubINAPPStoreProduct import com.revenuecat.purchases.utils.stubPricingPhase @@ -553,6 +558,42 @@ class OfferingsTest { assertThat(offerings.targeting.ruleId).isEqualTo("abc123") } + @Test + fun `createOfferings creates UiConfig object`() { + // Arrange + val storeProductMonthly = getStoreProduct(productIdentifier, monthlyPeriod, monthlyBasePlanId) + val storeProductAnnual = getStoreProduct(productIdentifier, annualPeriod, annualBasePlanId) + val products = mapOf(productIdentifier to listOf(storeProductMonthly, storeProductAnnual)) + val uiConfigJson = getUiConfigJson( + colors = mapOf("primary" to "#ff00ff"), + fonts = mapOf("primary" to FontInfo.Name("Roboto")), + localizations = mapOf("en_US" to mapOf(VariableLocalizationKey.MONTHLY to "monthly")), + variableCompatibilityMap = mapOf("new var" to "guaranteed var"), + functionCompatibilityMap = mapOf("new fun" to "guaranteed fun") + ) + val offeringsJson = getOfferingsJSON(uiConfig = uiConfigJson) + + // Act + val offerings = offeringsParser.createOfferings(offeringsJson, products) + + // Assert + assertThat(offerings).isNotNull + assertThat(offerings.all.size).isEqualTo(2) + assertThat(offerings.current!!.identifier).isEqualTo(offeringsJson.getString("current_offering_id")) + assertThat(offerings["offering_a"]).isNotNull + assertThat(offerings["offering_b"]).isNotNull + + assertThat(offerings.uiConfig).isNotNull + val colorInfo = offerings.uiConfig!!.app.colors[ColorAlias("primary")]!!.light as ColorInfo.Hex + assertThat(colorInfo.value).isEqualTo(parseRGBAColor("#ff00ff")) + val fontInfo = offerings.uiConfig.app.fonts[FontAlias("primary")]!!.android as FontInfo.Name + assertThat(fontInfo.value).isEqualTo("Roboto") + assertThat(offerings.uiConfig.localizations[LocaleId("en_US")]!![VariableLocalizationKey.MONTHLY]) + .isEqualTo("monthly") + assertThat(offerings.uiConfig.variableConfig.variableCompatibilityMap["new var"]).isEqualTo("guaranteed var") + assertThat(offerings.uiConfig.variableConfig.functionCompatibilityMap["new fun"]).isEqualTo("guaranteed fun") + } + private fun testPackageType(packageType: PackageType) { var identifier = packageType.identifier if (identifier == null) { @@ -630,6 +671,7 @@ class OfferingsTest { ), placements: JSONObject? = null, targeting: JSONObject? = null, + uiConfig: JSONObject? = null, ): JSONObject { val offeringJsons = mutableListOf() offeringPackagesById.forEach { (offeringId, packages) -> @@ -649,6 +691,7 @@ class OfferingsTest { put("current_offering_id", currentOfferingId) placements?.let { put("placements", placements) } targeting?.let { put("targeting", placements) } + uiConfig?.let { put("ui_config", uiConfig) } } } @@ -676,6 +719,100 @@ class OfferingsTest { } } + /** + * @param colors Color alias (e.g. "primary") to hex color. In reality we support an entire ColorScheme. + * @param fonts Font alias (e.g. "primary") to FontInfo. + * @param localizations LocaleId (e.g. "en_US") to a map of VariableLocalizationKey to its localized value. + * @param variableCompatibilityMap Map of new variables to guaranteed-to-be-available variables. + * @param functionCompatibilityMap Map of new functions to guaranteed-to-be-available functions. + */ + private fun getUiConfigJson( + colors: Map, + fonts: Map, + localizations: Map>, + variableCompatibilityMap: Map = emptyMap(), + functionCompatibilityMap: Map = emptyMap(), + ): JSONObject { + return JSONObject().apply { + put( + "app", + JSONObject().apply { + put( + "colors", + JSONObject().apply { + colors.forEach { (colorAlias, color) -> + put( + colorAlias, + JSONObject().apply { + put( + "light", + JSONObject().apply { + put("type", "hex") + put("value", color) + } + ) + } + ) + } + } + ) + put( + "fonts", + JSONObject().apply { + fonts.forEach { (fontAlias, fontInfo) -> + put( + fontAlias, + JSONObject().apply { + val fontInfoJson = JSONObject().apply { + when (fontInfo) { + is FontInfo.Name -> { + put("type", "name") + put("value", fontInfo.value) + } + + is FontInfo.GoogleFonts -> { + put("type", "google_fonts") + put("value", fontInfo.value) + } + } + } + put("android", fontInfoJson) + } + ) + } + } + ) + } + ) + put( + "localizations", + JSONObject().apply { + localizations.forEach { (localeId, variableLocalizations) -> + put( + localeId, + JSONObject().apply { + variableLocalizations.forEach { (key, value) -> put(key.name.lowercase(), value) } + } + ) + } + } + ) + put( + "variable_config", + JSONObject().apply { + put( + "variable_compatibility_map", + JSONObject().apply { variableCompatibilityMap.forEach { (key, value) -> put(key, value) } } + ) + put( + "function_compatibility_map", + JSONObject().apply { functionCompatibilityMap.forEach { (key, value) -> put(key, value) } } + ) + } + ) + } + } + private fun getOfferingJSON( offeringIdentifier: String = "offering_a", packagesJSON: List = listOf( diff --git a/purchases/src/test/java/com/revenuecat/purchases/UiConfigTests.kt b/purchases/src/test/java/com/revenuecat/purchases/UiConfigTests.kt new file mode 100644 index 0000000000..e3cc610eec --- /dev/null +++ b/purchases/src/test/java/com/revenuecat/purchases/UiConfigTests.kt @@ -0,0 +1,177 @@ +package com.revenuecat.purchases + +import com.revenuecat.purchases.common.OfferingParser +import com.revenuecat.purchases.paywalls.components.common.LocaleId +import com.revenuecat.purchases.paywalls.components.common.VariableLocalizationKey +import com.revenuecat.purchases.paywalls.components.properties.ColorInfo +import com.revenuecat.purchases.paywalls.components.properties.ColorScheme +import com.revenuecat.purchases.paywalls.parseRGBAColor +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +internal class UiConfigTests { + + @Test + fun `Should properly deserialize UiConfig`() { + // Arrange + // language=json + val serialized = """ + { + "app": { + "colors": { + "primary": { + "light": { + "type": "hex", + "value": "#ffcc00" + } + }, + "secondary": { + "light": { + "type": "linear", + "degrees": 45, + "points": [ + { + "color": "#032400ff", + "percent": 0 + }, + { + "color": "#090979ff", + "percent": 35 + }, + { + "color": "#216c32ff", + "percent": 100 + } + ] + } + }, + "tertiary": { + "light": { + "type": "radial", + "points": [ + { + "color": "#032400ff", + "percent": 0 + }, + { + "color": "#090979ff", + "percent": 35 + }, + { + "color": "#216c32ff", + "percent": 100 + } + ] + } + } + }, + "fonts": { + "primary": { + "ios": { + "type": "name", + "value": "SF Pro" + }, + "android": { + "type": "name", + "value": "Roboto" + }, + "web": { + "type": "google_fonts", + "value": "Gothic" + } + } + } + }, + "localizations": { + "en_US": { + "monthly": "monthly" + }, + "es_ES": { + "monthly": "mensual" + } + }, + "variable_config": { + "variable_compatibility_map": { + "new var": "guaranteed var" + }, + "function_compatibility_map": { + "new fun": "guaranteed fun" + } + } + } + """.trimIndent() + val expected = UiConfig( + app = UiConfig.AppConfig( + colors = mapOf( + ColorAlias("primary") to ColorScheme( + light = ColorInfo.Hex(parseRGBAColor("#ffcc00")), + ), + ColorAlias("secondary") to ColorScheme( + light = ColorInfo.Gradient.Linear( + degrees = 45f, + points = listOf( + ColorInfo.Gradient.Point( + color = parseRGBAColor("#032400ff"), + percent = 0f, + ), + ColorInfo.Gradient.Point( + color = parseRGBAColor("#090979ff"), + percent = 35f, + ), + ColorInfo.Gradient.Point( + color = parseRGBAColor("#216c32ff"), + percent = 100f, + ) + ) + ) + ), + ColorAlias("tertiary") to ColorScheme( + light = ColorInfo.Gradient.Radial( + points = listOf( + ColorInfo.Gradient.Point( + color = parseRGBAColor("#032400ff"), + percent = 0f, + ), + ColorInfo.Gradient.Point( + color = parseRGBAColor("#090979ff"), + percent = 35f, + ), + ColorInfo.Gradient.Point( + color = parseRGBAColor("#216c32ff"), + percent = 100f, + ) + ) + ) + ) + ), + fonts = mapOf( + FontAlias("primary") to UiConfig.AppConfig.FontsConfig( + android = UiConfig.AppConfig.FontsConfig.FontInfo.Name("Roboto"), + ) + ) + ), + localizations = mapOf( + LocaleId("en_US") to mapOf( + VariableLocalizationKey.MONTHLY to "monthly" + ), + LocaleId("es_ES") to mapOf( + VariableLocalizationKey.MONTHLY to "mensual" + ) + ), + variableConfig = UiConfig.VariableConfig( + variableCompatibilityMap = mapOf( + "new var" to "guaranteed var" + ), + functionCompatibilityMap = mapOf( + "new fun" to "guaranteed fun" + ) + ) + ) + + // Act + val actual = OfferingParser.json.decodeFromString(serialized) + + // Assert + assertThat(actual).isEqualTo(expected) + } +} diff --git a/purchases/src/test/java/com/revenuecat/purchases/paywalls/components/common/VariableLocalizationKeyTests.kt b/purchases/src/test/java/com/revenuecat/purchases/paywalls/components/common/VariableLocalizationKeyTests.kt new file mode 100644 index 0000000000..a70633531d --- /dev/null +++ b/purchases/src/test/java/com/revenuecat/purchases/paywalls/components/common/VariableLocalizationKeyTests.kt @@ -0,0 +1,75 @@ +package com.revenuecat.purchases.paywalls.components.common + +import com.revenuecat.purchases.common.OfferingParser +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +internal class VariableLocalizationKeyTests( + private val serialized: String, + private val expected: VariableLocalizationKey, +) { + + companion object { + @Suppress("LongMethod") + @JvmStatic + @Parameterized.Parameters(name = "{0}") + fun parameters(): Collection<*> = VariableLocalizationKey.values().map { expected -> + val serialized = when (expected) { + VariableLocalizationKey.DAY -> "\"day\"" + VariableLocalizationKey.DAILY -> "\"daily\"" + VariableLocalizationKey.DAY_SHORT -> "\"day_short\"" + VariableLocalizationKey.WEEK -> "\"week\"" + VariableLocalizationKey.WEEKLY -> "\"weekly\"" + VariableLocalizationKey.WEEK_SHORT -> "\"week_short\"" + VariableLocalizationKey.MONTH -> "\"month\"" + VariableLocalizationKey.MONTHLY -> "\"monthly\"" + VariableLocalizationKey.MONTH_SHORT -> "\"month_short\"" + VariableLocalizationKey.YEAR -> "\"year\"" + VariableLocalizationKey.YEARLY -> "\"yearly\"" + VariableLocalizationKey.YEAR_SHORT -> "\"year_short\"" + VariableLocalizationKey.ANNUAL -> "\"annual\"" + VariableLocalizationKey.ANNUALLY -> "\"annually\"" + VariableLocalizationKey.ANNUAL_SHORT -> "\"annual_short\"" + VariableLocalizationKey.FREE_PRICE -> "\"free_price\"" + VariableLocalizationKey.PERCENT -> "\"percent\"" + VariableLocalizationKey.NUM_DAY_ZERO -> "\"num_day_zero\"" + VariableLocalizationKey.NUM_DAY_ONE -> "\"num_day_one\"" + VariableLocalizationKey.NUM_DAY_TWO -> "\"num_day_two\"" + VariableLocalizationKey.NUM_DAY_FEW -> "\"num_day_few\"" + VariableLocalizationKey.NUM_DAY_MANY -> "\"num_day_many\"" + VariableLocalizationKey.NUM_DAY_OTHER -> "\"num_day_other\"" + VariableLocalizationKey.NUM_WEEK_ZERO -> "\"num_week_zero\"" + VariableLocalizationKey.NUM_WEEK_ONE -> "\"num_week_one\"" + VariableLocalizationKey.NUM_WEEK_TWO -> "\"num_week_two\"" + VariableLocalizationKey.NUM_WEEK_FEW -> "\"num_week_few\"" + VariableLocalizationKey.NUM_WEEK_MANY -> "\"num_week_many\"" + VariableLocalizationKey.NUM_WEEK_OTHER -> "\"num_week_other\"" + VariableLocalizationKey.NUM_MONTH_ZERO -> "\"num_month_zero\"" + VariableLocalizationKey.NUM_MONTH_ONE -> "\"num_month_one\"" + VariableLocalizationKey.NUM_MONTH_TWO -> "\"num_month_two\"" + VariableLocalizationKey.NUM_MONTH_FEW -> "\"num_month_few\"" + VariableLocalizationKey.NUM_MONTH_MANY -> "\"num_month_many\"" + VariableLocalizationKey.NUM_MONTH_OTHER -> "\"num_month_other\"" + VariableLocalizationKey.NUM_YEAR_ZERO -> "\"num_year_zero\"" + VariableLocalizationKey.NUM_YEAR_ONE -> "\"num_year_one\"" + VariableLocalizationKey.NUM_YEAR_TWO -> "\"num_year_two\"" + VariableLocalizationKey.NUM_YEAR_FEW -> "\"num_year_few\"" + VariableLocalizationKey.NUM_YEAR_MANY -> "\"num_year_many\"" + VariableLocalizationKey.NUM_YEAR_OTHER -> "\"num_year_other\"" + } + arrayOf(serialized, expected) + } + } + + @Test + fun `Should properly deserialize VariableLocalizationKey`() { + // Arrange, Act + val actual = OfferingParser.json.decodeFromString(serialized) + + // Assert + assert(actual == expected) + } + +} From dfd75f2106ed928508fe0a88c2c240fb10199db0 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Fri, 17 Jan 2025 18:51:11 +0100 Subject: [PATCH 02/16] Moves UiConfig from Offerings to Offering. --- .../com/revenuecat/purchases/Offering.kt | 9 +- .../com/revenuecat/purchases/Offerings.kt | 5 +- .../purchases/common/OfferingParser.kt | 41 ++++--- .../com/revenuecat/purchases/OfferingsTest.kt | 101 +++++++++++++----- .../purchases/amazon/AmazonOfferingsTest.kt | 4 +- 5 files changed, 110 insertions(+), 50 deletions(-) diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/Offering.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/Offering.kt index 92f0832315..56e64c0a72 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/Offering.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/Offering.kt @@ -7,6 +7,7 @@ package com.revenuecat.purchases import com.revenuecat.purchases.paywalls.PaywallData import com.revenuecat.purchases.paywalls.components.common.PaywallComponentsData +import dev.drewhamilton.poko.Poko /** * An offering is a collection of [Package] available for the user to purchase. @@ -27,8 +28,14 @@ constructor( val availablePackages: List, val paywall: PaywallData? = null, @InternalRevenueCatAPI - val paywallComponents: PaywallComponentsData? = null, + val paywallComponents: PaywallComponents? = null, ) { + @InternalRevenueCatAPI + @Poko + class PaywallComponents( + val uiConfig: UiConfig, + val data: PaywallComponentsData, + ) /** * Lifetime package type configured in the RevenueCat dashboard, if available. diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/Offerings.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/Offerings.kt index baad605f16..ff55e27c38 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/Offerings.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/Offerings.kt @@ -6,14 +6,11 @@ package com.revenuecat.purchases * @property current Current offering configured in the RevenueCat dashboard. * @property all Dictionary of all Offerings [Offering] objects keyed by their identifier. */ -data class Offerings -@OptIn(InternalRevenueCatAPI::class) -internal constructor( +data class Offerings internal constructor( val current: Offering?, val all: Map, internal val placements: Placements? = null, internal val targeting: Targeting? = null, - internal val uiConfig: UiConfig? = null, ) { constructor(current: Offering?, all: Map) : this(current, all, null, null) 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 0a2de8a9db..ee492c85fc 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/common/OfferingParser.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/common/OfferingParser.kt @@ -46,10 +46,22 @@ internal abstract class OfferingParser { val jsonOfferings = offeringsJson.getJSONArray("offerings") val currentOfferingID = offeringsJson.getString("current_offering_id") + val uiConfigJson = offeringsJson.optJSONObject("ui_config") + + @Suppress("TooGenericExceptionCaught") + val uiConfig: UiConfig? = uiConfigJson?.let { + try { + json.decodeFromString(it.toString()) + } catch (e: Throwable) { + errorLog("Error deserializing ui_config", e) + null + } + } + val offerings = mutableMapOf() for (i in 0 until jsonOfferings.length()) { val offeringJson = jsonOfferings.getJSONObject(i) - createOffering(offeringJson, productsById)?.let { + createOffering(offeringJson, productsById, uiConfig)?.let { offerings[it.identifier] = it if (it.availablePackages.isEmpty()) { @@ -86,30 +98,21 @@ internal abstract class OfferingParser { } } - val uiConfigJson = offeringsJson.optJSONObject("ui_config") - - @Suppress("TooGenericExceptionCaught") - val uiConfig: UiConfig? = uiConfigJson?.let { - try { - json.decodeFromString(it.toString()) - } catch (e: Throwable) { - errorLog("Error deserializing ui_config", e) - null - } - } - return Offerings( current = offerings[currentOfferingID]?.withPresentedContext(null, targeting), all = offerings, placements = placements, targeting = targeting, - uiConfig = uiConfig, ) } @OptIn(InternalRevenueCatAPI::class) @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - fun createOffering(offeringJson: JSONObject, productsById: Map>): Offering? { + fun createOffering( + offeringJson: JSONObject, + productsById: Map>, + uiConfig: UiConfig?, + ): Offering? { val offeringIdentifier = offeringJson.getString("identifier") val metadata = offeringJson.optJSONObject("metadata")?.toMap(deep = true) ?: emptyMap() val jsonPackages = offeringJson.getJSONArray("packages") @@ -150,6 +153,12 @@ internal abstract class OfferingParser { null } + val paywallComponents = if (paywallComponentsData != null && uiConfig != null) { + Offering.PaywallComponents(uiConfig, paywallComponentsData) + } else { + null + } + return if (availablePackages.isNotEmpty()) { Offering( offeringIdentifier, @@ -157,7 +166,7 @@ internal abstract class OfferingParser { metadata, availablePackages, paywallData, - paywallComponentsData, + paywallComponents, ) } else { null diff --git a/purchases/src/test/java/com/revenuecat/purchases/OfferingsTest.kt b/purchases/src/test/java/com/revenuecat/purchases/OfferingsTest.kt index 5f870210f5..e98ef572dd 100644 --- a/purchases/src/test/java/com/revenuecat/purchases/OfferingsTest.kt +++ b/purchases/src/test/java/com/revenuecat/purchases/OfferingsTest.kt @@ -20,6 +20,7 @@ import com.revenuecat.purchases.utils.stubPricingPhase import com.revenuecat.purchases.utils.stubStoreProduct import com.revenuecat.purchases.utils.stubSubscriptionOption import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.fail import org.json.JSONArray import org.json.JSONObject import org.junit.Test @@ -221,7 +222,7 @@ class OfferingsTest { val products = mapOf(productId to listOf(storeProductAnnual)) val offeringWithOneMonthlyPackageJson = getOfferingJSON() - val offering = offeringsParser.createOffering(offeringWithOneMonthlyPackageJson, products) + val offering = offeringsParser.createOffering(offeringWithOneMonthlyPackageJson, products, null) assertThat(offering).isNull() } @@ -270,7 +271,7 @@ class OfferingsTest { metadata = metadata ) - val offering = offeringsParser.createOffering(offeringJSON, products) + val offering = offeringsParser.createOffering(offeringJSON, products, null) assertThat(offering).isNotNull assertThat(offering!!.identifier).isEqualTo(offeringId) assertThat(offering!!.metadata).isEqualTo(metadata) @@ -571,27 +572,27 @@ class OfferingsTest { variableCompatibilityMap = mapOf("new var" to "guaranteed var"), functionCompatibilityMap = mapOf("new fun" to "guaranteed fun") ) - val offeringsJson = getOfferingsJSON(uiConfig = uiConfigJson) + val offeringJson = getOfferingJSON(paywallComponents = getPaywallComponentsDataJson()) + val offeringsJson = getOfferingsJSON(offerings = JSONArray(listOf(offeringJson)), uiConfig = uiConfigJson) // Act val offerings = offeringsParser.createOfferings(offeringsJson, products) // Assert assertThat(offerings).isNotNull - assertThat(offerings.all.size).isEqualTo(2) - assertThat(offerings.current!!.identifier).isEqualTo(offeringsJson.getString("current_offering_id")) - assertThat(offerings["offering_a"]).isNotNull - assertThat(offerings["offering_b"]).isNotNull + assertThat(offerings.all.size).isEqualTo(1) + val offering = offerings.all.values.first() - assertThat(offerings.uiConfig).isNotNull - val colorInfo = offerings.uiConfig!!.app.colors[ColorAlias("primary")]!!.light as ColorInfo.Hex + val paywallComponents = offering.paywallComponents ?: fail("paywallComponents is null") + val uiConfig = paywallComponents.uiConfig + val colorInfo = uiConfig.app.colors[ColorAlias("primary")]!!.light as ColorInfo.Hex assertThat(colorInfo.value).isEqualTo(parseRGBAColor("#ff00ff")) - val fontInfo = offerings.uiConfig.app.fonts[FontAlias("primary")]!!.android as FontInfo.Name + val fontInfo = uiConfig.app.fonts[FontAlias("primary")]!!.android as FontInfo.Name assertThat(fontInfo.value).isEqualTo("Roboto") - assertThat(offerings.uiConfig.localizations[LocaleId("en_US")]!![VariableLocalizationKey.MONTHLY]) + assertThat(uiConfig.localizations[LocaleId("en_US")]!![VariableLocalizationKey.MONTHLY]) .isEqualTo("monthly") - assertThat(offerings.uiConfig.variableConfig.variableCompatibilityMap["new var"]).isEqualTo("guaranteed var") - assertThat(offerings.uiConfig.variableConfig.functionCompatibilityMap["new fun"]).isEqualTo("guaranteed fun") + assertThat(uiConfig.variableConfig.variableCompatibilityMap["new var"]).isEqualTo("guaranteed var") + assertThat(uiConfig.variableConfig.functionCompatibilityMap["new fun"]).isEqualTo("guaranteed fun") } private fun testPackageType(packageType: PackageType) { @@ -686,14 +687,29 @@ class OfferingsTest { val offeringsJsonArray = JSONArray(offeringJsons) - return JSONObject().apply { - put("offerings", offeringsJsonArray) + return getOfferingsJSON( + offerings = offeringsJsonArray, + currentOfferingId = currentOfferingId, + placements = placements, + targeting = targeting, + uiConfig = uiConfig, + ) + } + + private fun getOfferingsJSON( + offerings: JSONArray, + currentOfferingId: String = "offering_a", + placements: JSONObject? = null, + targeting: JSONObject? = null, + uiConfig: JSONObject? = null, + ): JSONObject = + JSONObject().apply { + put("offerings", offerings) put("current_offering_id", currentOfferingId) placements?.let { put("placements", placements) } targeting?.let { put("targeting", placements) } uiConfig?.let { put("ui_config", uiConfig) } } - } private fun getPlacementsJSON( fallbackOfferingId: String?, @@ -813,6 +829,39 @@ class OfferingsTest { } } + private fun getPaywallComponentsDataJson() = JSONObject( + // language=json + """ + { + "template_name": "components", + "asset_base_url": "https://assets.pawwalls.com", + "components_config": { + "base": { + "stack": { + "type": "stack", + "components": [] + }, + "background": { + "type": "color", + "value": { + "light": { + "type": "alias", + "value": "primary" + } + } + } + } + }, + "components_localizations": { + "en_US": { + "ZvS4Ck5hGM": "Hello" + } + }, + "default_locale": "en_US" + } + """.trimIndent() + ) + private fun getOfferingJSON( offeringIdentifier: String = "offering_a", packagesJSON: List = listOf( @@ -822,17 +871,15 @@ class OfferingsTest { monthlyBasePlanId ), ), - metadata: Map? = null - ) = JSONObject( - """ - { - 'description': 'This is the base offering', - 'identifier': '$offeringIdentifier', - 'packages': $packagesJSON, - 'metadata': ${if (metadata != null) JSONObject(metadata).toString() else "null"} - } - """.trimIndent() - ) + metadata: Map? = null, + paywallComponents: JSONObject? = null, + ) = JSONObject().apply { + put("description", "This is the base offering") + put("identifier", offeringIdentifier) + put("packages", JSONArray(packagesJSON)) + if (metadata != null) put("metadata", JSONObject(metadata)) else put("metadata", "null") + if (paywallComponents != null) put("paywall_components", paywallComponents) + } private fun getOfferingJSONWithoutMetadata( offeringIdentifier: String = "offering_a", diff --git a/purchases/src/test/java/com/revenuecat/purchases/amazon/AmazonOfferingsTest.kt b/purchases/src/test/java/com/revenuecat/purchases/amazon/AmazonOfferingsTest.kt index ff5052a3b7..767425c12c 100644 --- a/purchases/src/test/java/com/revenuecat/purchases/amazon/AmazonOfferingsTest.kt +++ b/purchases/src/test/java/com/revenuecat/purchases/amazon/AmazonOfferingsTest.kt @@ -165,7 +165,7 @@ class AmazonOfferingsTest { val products = mapOf(productId to listOf(storeProductAnnual)) val offeringWithOneMonthlyPackageJson = getAmazonOfferingJSON() - val offering = offeringsParser.createOffering(offeringWithOneMonthlyPackageJson, products) + val offering = offeringsParser.createOffering(offeringWithOneMonthlyPackageJson, products, null) Assertions.assertThat(offering).isNull() } @@ -193,7 +193,7 @@ class AmazonOfferingsTest { packagesJSON = listOf(monthlyPackageJSON, annualPackageJSON) ) - val offering = offeringsParser.createOffering(offeringJSON, products) + val offering = offeringsParser.createOffering(offeringJSON, products, null) Assertions.assertThat(offering).isNotNull Assertions.assertThat(offering!!.identifier).isEqualTo(offeringId) From f0fabc835b9fa7cc32414fef91b19ffe5406e66f Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Fri, 17 Jan 2025 19:07:20 +0100 Subject: [PATCH 03/16] Fixes compilation of revenuecatui. --- .../com/revenuecat/purchases/UiConfig.kt | 4 ++-- .../components/LoadedPaywallComponents.kt | 5 +++-- .../components/button/ButtonComponentView.kt | 3 ++- .../components/image/ImageComponentView.kt | 3 ++- .../components/stack/StackComponentView.kt | 3 ++- .../components/text/TextComponentView.kt | 3 ++- .../ui/revenuecatui/data/PaywallViewModel.kt | 2 +- .../helpers/OfferingToStateMapper.kt | 12 ++++++------ .../PaywallComponentDataValidationTests.kt | 9 +++++---- .../LoadedPaywallComponentsLocaleTests.kt | 3 ++- .../components/PaywallActionTests.kt | 3 ++- .../pkg/PackageComponentViewTests.kt | 5 +++-- .../stack/StackComponentViewTests.kt | 5 +++-- .../components/text/TextComponentViewTests.kt | 9 +++++---- .../revenuecatui/data/PaywallViewModelTest.kt | 19 ++++++++++--------- .../revenuecatui/helpers/FakePaywallState.kt | 3 ++- 16 files changed, 52 insertions(+), 39 deletions(-) diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/UiConfig.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/UiConfig.kt index 87dfc70fcd..3527059f0f 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/UiConfig.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/UiConfig.kt @@ -22,12 +22,12 @@ value class FontAlias(@get:JvmSynthetic val value: String) @Serializable class UiConfig( @get:JvmSynthetic - val app: AppConfig, + val app: AppConfig = AppConfig(), @get:JvmSynthetic val localizations: Map> = emptyMap(), @SerialName("variable_config") @get:JvmSynthetic - val variableConfig: VariableConfig, + val variableConfig: VariableConfig = VariableConfig(), ) { @InternalRevenueCatAPI 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 26e7dce603..0ad22215e8 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 @@ -15,6 +15,7 @@ import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.tooling.preview.Preview import com.revenuecat.purchases.Offering +import com.revenuecat.purchases.UiConfig import com.revenuecat.purchases.paywalls.components.StackComponent import com.revenuecat.purchases.paywalls.components.StickyFooterComponent import com.revenuecat.purchases.paywalls.components.TextComponent @@ -156,7 +157,7 @@ private fun LoadedPaywallComponents_Preview() { serverDescription = "description", metadata = emptyMap(), availablePackages = emptyList(), - paywallComponents = data, + paywallComponents = Offering.PaywallComponents(UiConfig(), data), ) val validated = offering.validatePaywallComponentsDataOrNull()?.getOrThrow()!! val state = offering.toComponentsPaywallState( @@ -335,7 +336,7 @@ private fun LoadedPaywallComponents_Preview_Bless() { serverDescription = "description", metadata = emptyMap(), availablePackages = emptyList(), - paywallComponents = data, + paywallComponents = Offering.PaywallComponents(UiConfig(), data), ) val validated = offering.validatePaywallComponentsDataOrNull()?.getOrThrow()!! val state = offering.toComponentsPaywallState( 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 a24ef18d5a..5dca8f4509 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 @@ -16,6 +16,7 @@ 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.UiConfig import com.revenuecat.purchases.paywalls.components.StackComponent import com.revenuecat.purchases.paywalls.components.common.Background import com.revenuecat.purchases.paywalls.components.common.ComponentsConfig @@ -164,7 +165,7 @@ private fun previewEmptyState(): PaywallState.Loaded.Components { serverDescription = "serverDescription", metadata = emptyMap(), availablePackages = emptyList(), - paywallComponents = data, + paywallComponents = Offering.PaywallComponents(UiConfig(), data), ) val validated = offering.validatePaywallComponentsDataOrNull()?.getOrThrow()!! return offering.toComponentsPaywallState( diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/image/ImageComponentView.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/image/ImageComponentView.kt index 7c251761f3..fd701f76af 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/image/ImageComponentView.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/image/ImageComponentView.kt @@ -38,6 +38,7 @@ import coil.ImageLoader import coil.decode.DataSource import coil.request.SuccessResult import com.revenuecat.purchases.Offering +import com.revenuecat.purchases.UiConfig import com.revenuecat.purchases.paywalls.components.StackComponent import com.revenuecat.purchases.paywalls.components.common.Background import com.revenuecat.purchases.paywalls.components.common.ComponentsConfig @@ -467,7 +468,7 @@ private fun previewEmptyState(): PaywallState.Loaded.Components { serverDescription = "serverDescription", metadata = emptyMap(), availablePackages = emptyList(), - paywallComponents = data, + paywallComponents = Offering.PaywallComponents(UiConfig(), data), ) val validated = offering.validatePaywallComponentsDataOrNull()?.getOrThrow()!! return offering.toComponentsPaywallState( 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 8d5c684045..05816878b1 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 @@ -35,6 +35,7 @@ import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.revenuecat.purchases.Offering +import com.revenuecat.purchases.UiConfig import com.revenuecat.purchases.paywalls.components.StackComponent import com.revenuecat.purchases.paywalls.components.common.Background import com.revenuecat.purchases.paywalls.components.common.ComponentsConfig @@ -1085,7 +1086,7 @@ private fun previewEmptyState(): PaywallState.Loaded.Components { serverDescription = "serverDescription", metadata = emptyMap(), availablePackages = emptyList(), - paywallComponents = data, + paywallComponents = Offering.PaywallComponents(UiConfig(), data), ) val validated = offering.validatePaywallComponentsDataOrNull()?.getOrThrow()!! return offering.toComponentsPaywallState( 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 2acc43e855..d5b6b506bd 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 @@ -20,6 +20,7 @@ import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.revenuecat.purchases.Offering +import com.revenuecat.purchases.UiConfig import com.revenuecat.purchases.paywalls.components.StackComponent import com.revenuecat.purchases.paywalls.components.common.Background import com.revenuecat.purchases.paywalls.components.common.ComponentsConfig @@ -454,7 +455,7 @@ private fun previewEmptyState(): PaywallState.Loaded.Components { serverDescription = "serverDescription", metadata = emptyMap(), availablePackages = emptyList(), - paywallComponents = data, + paywallComponents = Offering.PaywallComponents(UiConfig(), data), ) val validated = offering.validatePaywallComponentsDataOrNull()?.getOrThrow()!! return offering.toComponentsPaywallState( 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 4ff31fd232..b97a69e36c 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 @@ -507,7 +507,7 @@ internal class PaywallViewModelImpl( } return PaywallEvent.Data( offeringIdentifier = offering.identifier, - paywallRevision = paywallData.revision, + paywallRevision = paywallData.data.revision, sessionIdentifier = UUID.randomUUID(), displayMode = mode.name.lowercase(), localeIdentifier = locale.toString(), 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 4b577ea482..632f363550 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 @@ -90,15 +90,15 @@ internal fun Offering.validatePaywallComponentsDataOrNull(): RcResult return RcResult.Error(error) } // Build a NonEmptyMap of localizations, ensuring that we always have the default localization as fallback. val localizations = nonEmptyMapOf( - paywallComponents.defaultLocaleIdentifier to defaultLocalization, - paywallComponents.componentsLocalizations, + paywallComponents.data.defaultLocaleIdentifier to defaultLocalization, + paywallComponents.data.componentsLocalizations, ).mapValues { (locale, map) -> // We need to turn our NonEmptyMap into NonEmptyMap. If a certain locale // has an empty Map, we add an AllLocalizationsMissing error for that locale to our list of errors. @@ -110,7 +110,7 @@ internal fun Offering.validatePaywallComponentsDataOrNull(): RcResult Date: Tue, 21 Jan 2025 12:06:43 +0100 Subject: [PATCH 04/16] Ignores unknown VariableLocalizationKeys in the map. --- .../com/revenuecat/purchases/UiConfig.kt | 2 + .../components/common/Localization.kt | 52 ++++++++- .../purchases/utils/MapExtensions.kt | 11 ++ .../com/revenuecat/purchases/UiConfigTests.kt | 103 ++++++++++++++++++ 4 files changed, 167 insertions(+), 1 deletion(-) diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/UiConfig.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/UiConfig.kt index 3527059f0f..9e7f8fe9a6 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/UiConfig.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/UiConfig.kt @@ -1,6 +1,7 @@ package com.revenuecat.purchases import com.revenuecat.purchases.paywalls.components.common.LocaleId +import com.revenuecat.purchases.paywalls.components.common.LocalizedVariableLocalizationKeyMapSerializer import com.revenuecat.purchases.paywalls.components.common.VariableLocalizationKey import com.revenuecat.purchases.paywalls.components.properties.ColorScheme import dev.drewhamilton.poko.Poko @@ -23,6 +24,7 @@ value class FontAlias(@get:JvmSynthetic val value: String) class UiConfig( @get:JvmSynthetic val app: AppConfig = AppConfig(), + @Serializable(with = LocalizedVariableLocalizationKeyMapSerializer::class) @get:JvmSynthetic val localizations: Map> = emptyMap(), @SerialName("variable_config") 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 2711d08cb5..abfd79afda 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 @@ -2,12 +2,15 @@ package com.revenuecat.purchases.paywalls.components.common import com.revenuecat.purchases.InternalRevenueCatAPI import com.revenuecat.purchases.paywalls.components.properties.ThemeImageUrls +import com.revenuecat.purchases.utils.mapNotNullKeys import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.SerializationException +import kotlinx.serialization.builtins.MapSerializer +import kotlinx.serialization.builtins.serializer import kotlinx.serialization.descriptors.PolymorphicKind import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.buildSerialDescriptor @@ -66,13 +69,60 @@ private object LocalizationDataSerializer : KSerializer { } } +/** + * Deserializes a map of [LocaleId] to [VariableLocalizationKey] maps. The [VariableLocalizationKey] maps ignore unknown + * [VariableLocalizationKey]s. + */ +@Suppress("MaxLineLength") +@InternalRevenueCatAPI +internal object LocalizedVariableLocalizationKeyMapSerializer : KSerializer>> { + private val delegate = MapSerializer( + keySerializer = LocaleId.serializer(), + valueSerializer = VariableLocalizationKeyMapSerializer, + ) + override val descriptor: SerialDescriptor = delegate.descriptor + + override fun serialize(encoder: Encoder, value: Map>) { + // Serialization is not implemented as it is not needed. + } + + override fun deserialize(decoder: Decoder): Map> = + delegate.deserialize(decoder) +} + +/** + * Deserializes a map of [VariableLocalizationKey]s to String, ignoring unknown [VariableLocalizationKey]s. + */ +@InternalRevenueCatAPI +internal object VariableLocalizationKeyMapSerializer : KSerializer> { + private val delegate = MapSerializer(String.serializer(), String.serializer()) + + // We can use mapSerialDescriptor() when that is no longer experimental. For now + // using the delegate's descriptor is good enough, even though that has String keys. + override val descriptor: SerialDescriptor = delegate.descriptor + + override fun serialize(encoder: Encoder, value: Map) { + // Serialization is not implemented as it is not needed. + } + + override fun deserialize(decoder: Decoder): Map = + decoder.decodeSerializableValue(delegate).mapNotNullKeys { (stringKey, _) -> + @Suppress("SwallowedException") + try { + VariableLocalizationKey.valueOf(stringKey.uppercase()) + } catch (e: IllegalArgumentException) { + // Ignoring unknown VariableLocalizationKey. + null + } + } +} + /** * Keys for localized strings used as variable values. */ @InternalRevenueCatAPI @Serializable enum class VariableLocalizationKey { - @SerialName("day") DAY, diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/utils/MapExtensions.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/utils/MapExtensions.kt index 596ebc4762..d573a81d99 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/utils/MapExtensions.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/utils/MapExtensions.kt @@ -3,3 +3,14 @@ package com.revenuecat.purchases.utils @Suppress("UNCHECKED_CAST") internal fun Map.filterNotNullValues(): Map = filterValues { it != null } as Map + +internal fun Map.mapNotNullKeys(transform: (Map.Entry) -> R?): Map { + val destination = LinkedHashMap(size) + forEach { entry -> + val transformedKey = transform(entry) + if (transformedKey != null) { + destination[transformedKey] = entry.value + } + } + return destination +} diff --git a/purchases/src/test/java/com/revenuecat/purchases/UiConfigTests.kt b/purchases/src/test/java/com/revenuecat/purchases/UiConfigTests.kt index e3cc610eec..2a30d3f1f1 100644 --- a/purchases/src/test/java/com/revenuecat/purchases/UiConfigTests.kt +++ b/purchases/src/test/java/com/revenuecat/purchases/UiConfigTests.kt @@ -174,4 +174,107 @@ internal class UiConfigTests { // Assert assertThat(actual).isEqualTo(expected) } + + @Test + fun `Should ignore unknown VariableLocalizationKeys`() { + // Arrange + // language=json + val serialized = """ + { + "localizations": { + "en_US": { + "monthly": "monthly", + "a_very_futuristic_key_we_dont_know_about": "unknown" + }, + "es_ES": { + "monthly": "mensual", + "a_very_futuristic_key_we_dont_know_about": "desconocido" + } + } + } + """.trimIndent() + val expected = UiConfig( + localizations = mapOf( + LocaleId("en_US") to mapOf( + VariableLocalizationKey.MONTHLY to "monthly" + ), + LocaleId("es_ES") to mapOf( + VariableLocalizationKey.MONTHLY to "mensual" + ) + ), + ) + + // Act + val actual = OfferingParser.json.decodeFromString(serialized) + + // Assert + assertThat(actual).isEqualTo(expected) + } + + @Test + fun `Should deserialize all known VariableLocalizationKeys`() { + // Arrange + // When new VariableLocalizationKeys are added, they should be added to the `serialized` JSON below. + // language=json + val serialized = """ + { + "localizations": { + "en_US": { + "day": "DAY", + "daily": "DAILY", + "day_short": "DAY_SHORT", + "week": "WEEK", + "weekly": "WEEKLY", + "week_short": "WEEK_SHORT", + "month": "MONTH", + "monthly": "MONTHLY", + "month_short": "MONTH_SHORT", + "year": "YEAR", + "yearly": "YEARLY", + "year_short": "YEAR_SHORT", + "annual": "ANNUAL", + "annually": "ANNUALLY", + "annual_short": "ANNUAL_SHORT", + "free_price": "FREE_PRICE", + "percent": "PERCENT", + "num_day_zero": "NUM_DAY_ZERO", + "num_day_one": "NUM_DAY_ONE", + "num_day_two": "NUM_DAY_TWO", + "num_day_few": "NUM_DAY_FEW", + "num_day_many": "NUM_DAY_MANY", + "num_day_other": "NUM_DAY_OTHER", + "num_week_zero": "NUM_WEEK_ZERO", + "num_week_one": "NUM_WEEK_ONE", + "num_week_two": "NUM_WEEK_TWO", + "num_week_few": "NUM_WEEK_FEW", + "num_week_many": "NUM_WEEK_MANY", + "num_week_other": "NUM_WEEK_OTHER", + "num_month_zero": "NUM_MONTH_ZERO", + "num_month_one": "NUM_MONTH_ONE", + "num_month_two": "NUM_MONTH_TWO", + "num_month_few": "NUM_MONTH_FEW", + "num_month_many": "NUM_MONTH_MANY", + "num_month_other": "NUM_MONTH_OTHER", + "num_year_zero": "NUM_YEAR_ZERO", + "num_year_one": "NUM_YEAR_ONE", + "num_year_two": "NUM_YEAR_TWO", + "num_year_few": "NUM_YEAR_FEW", + "num_year_many": "NUM_YEAR_MANY", + "num_year_other": "NUM_YEAR_OTHER" + } + } + } + """.trimIndent() + val expected = UiConfig( + localizations = mapOf( + LocaleId("en_US") to VariableLocalizationKey.values().associateWith { key -> key.name }, + ), + ) + + // Act + val actual = OfferingParser.json.decodeFromString(serialized) + + // Assert + assertThat(actual).isEqualTo(expected) + } } From eed558c7df23f1befeb44945a9b86a01cc97499f Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Tue, 21 Jan 2025 12:07:34 +0100 Subject: [PATCH 05/16] Adds a secondary Google Font to deserialize. --- .../com/revenuecat/purchases/UiConfigTests.kt | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/purchases/src/test/java/com/revenuecat/purchases/UiConfigTests.kt b/purchases/src/test/java/com/revenuecat/purchases/UiConfigTests.kt index 2a30d3f1f1..28d776c6b1 100644 --- a/purchases/src/test/java/com/revenuecat/purchases/UiConfigTests.kt +++ b/purchases/src/test/java/com/revenuecat/purchases/UiConfigTests.kt @@ -79,6 +79,20 @@ internal class UiConfigTests { "type": "google_fonts", "value": "Gothic" } + }, + "secondary": { + "ios": { + "type": "name", + "value": "Roboto" + }, + "android": { + "type": "google_fonts", + "value": "Gothic" + }, + "web": { + "type": "name", + "value": "SF Pro" + } } } }, @@ -147,7 +161,10 @@ internal class UiConfigTests { fonts = mapOf( FontAlias("primary") to UiConfig.AppConfig.FontsConfig( android = UiConfig.AppConfig.FontsConfig.FontInfo.Name("Roboto"), - ) + ), + FontAlias("secondary") to UiConfig.AppConfig.FontsConfig( + android = UiConfig.AppConfig.FontsConfig.FontInfo.GoogleFonts("Gothic"), + ), ) ), localizations = mapOf( From 3ea43a0edb16629f1671d0c0434b838e3dc347c2 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Tue, 21 Jan 2025 12:11:10 +0100 Subject: [PATCH 06/16] Fixes compilation of PaywallsTester. --- .../java/com/revenuecat/paywallstester/SamplePaywalls.kt | 8 +++++++- .../ui/screens/main/offerings/OfferingsScreen.kt | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) 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 cf0d10d5ab..6dd8b3f843 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 @@ -8,6 +8,7 @@ import com.revenuecat.purchases.InternalRevenueCatAPI import com.revenuecat.purchases.Offering import com.revenuecat.purchases.Package import com.revenuecat.purchases.PackageType +import com.revenuecat.purchases.UiConfig import com.revenuecat.purchases.models.Period import com.revenuecat.purchases.models.Price import com.revenuecat.purchases.models.TestStoreProduct @@ -46,7 +47,12 @@ class SamplePaywallsLoader { emptyMap(), SamplePaywalls.packages, paywall = (paywallForTemplate(template) as? SampleData.Legacy)?.data, - paywallComponents = (paywallForTemplate(template) as? SampleData.Components)?.data, + paywallComponents = (paywallForTemplate(template) as? SampleData.Components)?.data?.let { data -> + Offering.PaywallComponents( + uiConfig = UiConfig(), + data = data, + ) + }, ) } 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 7157761673..3ee21d0556 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 @@ -143,7 +143,7 @@ private fun OfferingsListScreen( offering.paywall?.also { Text("Template ${it.templateName}") } ?: offering.paywallComponents?.also { - Text("Components ${it.templateName}") + Text("Components ${it.data.templateName}") } ?: Text("No paywall") } } From 5b65840720c67e92b37a868551c2d03a1cd29109 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Tue, 21 Jan 2025 12:19:08 +0100 Subject: [PATCH 07/16] Updates a comment. --- .../src/test/java/com/revenuecat/purchases/UiConfigTests.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/purchases/src/test/java/com/revenuecat/purchases/UiConfigTests.kt b/purchases/src/test/java/com/revenuecat/purchases/UiConfigTests.kt index 28d776c6b1..031b775d3e 100644 --- a/purchases/src/test/java/com/revenuecat/purchases/UiConfigTests.kt +++ b/purchases/src/test/java/com/revenuecat/purchases/UiConfigTests.kt @@ -231,7 +231,8 @@ internal class UiConfigTests { @Test fun `Should deserialize all known VariableLocalizationKeys`() { // Arrange - // When new VariableLocalizationKeys are added, they should be added to the `serialized` JSON below. + // When new VariableLocalizationKeys are added, this test will fail. Make it pass by adding the new keys to the + // `serialized` JSON below. // language=json val serialized = """ { From e03d92e04be34612478578d534b12477dc39c597 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Tue, 21 Jan 2025 19:18:41 +0100 Subject: [PATCH 08/16] Some temporary CI debug options. --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index a2b1db57e1..0973ea570c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -180,7 +180,7 @@ jobs: command: ./gradlew :test-apps:testpurchasesuiandroidcompatibility:assembleDebug - run: name: Run Tests - command: ./gradlew lint test + command: java --version && echo $JAVA_HOME && ./gradlew lint test - run: name: Consolidate artifacts command: | From d7b1feeaf8b2d22c480e4440b8d85a9e0ffda3be Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Wed, 22 Jan 2025 08:52:01 +0100 Subject: [PATCH 09/16] Some more temporary CI debug options. --- .../test/java/com/revenuecat/purchases/OfferingsTest.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/purchases/src/test/java/com/revenuecat/purchases/OfferingsTest.kt b/purchases/src/test/java/com/revenuecat/purchases/OfferingsTest.kt index e98ef572dd..75c142ba89 100644 --- a/purchases/src/test/java/com/revenuecat/purchases/OfferingsTest.kt +++ b/purchases/src/test/java/com/revenuecat/purchases/OfferingsTest.kt @@ -575,14 +575,22 @@ class OfferingsTest { val offeringJson = getOfferingJSON(paywallComponents = getPaywallComponentsDataJson()) val offeringsJson = getOfferingsJSON(offerings = JSONArray(listOf(offeringJson)), uiConfig = uiConfigJson) + println("DEBUGGING - offeringsJson") + offeringsJson.toString(2).lines().forEach { println(it) } + // Act val offerings = offeringsParser.createOfferings(offeringsJson, products) + println("DEBUGGING - offerings") + offerings.toString().chunked(200).forEach { println(it) } // Assert assertThat(offerings).isNotNull assertThat(offerings.all.size).isEqualTo(1) val offering = offerings.all.values.first() + println("DEBUGGING - offering") + offerings.toString().chunked(200).forEach { println(it) } + val paywallComponents = offering.paywallComponents ?: fail("paywallComponents is null") val uiConfig = paywallComponents.uiConfig val colorInfo = uiConfig.app.colors[ColorAlias("primary")]!!.light as ColorInfo.Hex From 1e5fd646ae6f871e7c9f61d4d16122d32b05a9f6 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Wed, 22 Jan 2025 09:29:18 +0100 Subject: [PATCH 10/16] Fixes test artifact upload on CI. --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 0973ea570c..27ac285634 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -184,8 +184,8 @@ jobs: - run: name: Consolidate artifacts command: | - mkdir -p build/test-results/ - find . -type f -regex ".*/build/test-results/.*xml" -exec cp --parents {} build/test-results/ \; + mkdir -p build/reports/ + find . -type f -path "*/build/reports/*" | grep -v 'paparazzi' | xargs -I {} cp --parents {} build/reports/ - run: name: Kover HTML command: ./gradlew purchases:koverHtmlReportDefaultsRelease From 00998447f0225432e93f580a0e48bb379f40bf80 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Wed, 22 Jan 2025 10:00:55 +0100 Subject: [PATCH 11/16] Actually fixes test artifact upload on CI. --- .circleci/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 27ac285634..f2995df9bc 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -186,6 +186,7 @@ jobs: command: | mkdir -p build/reports/ find . -type f -path "*/build/reports/*" | grep -v 'paparazzi' | xargs -I {} cp --parents {} build/reports/ + when: always - run: name: Kover HTML command: ./gradlew purchases:koverHtmlReportDefaultsRelease From 0894ed3da41226ae9754aa0732dcf6f11ce8e20e Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Wed, 22 Jan 2025 10:35:02 +0100 Subject: [PATCH 12/16] Enables the paywall components build flag before running tests. --- .circleci/config.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index f2995df9bc..79a8ce2235 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -178,6 +178,9 @@ jobs: - run: name: Verify purchases-ui target SDK compatibility (currently 34) command: ./gradlew :test-apps:testpurchasesuiandroidcompatibility:assembleDebug + - run: + name: Enable the Paywall Components build flag + command: echo 'revenuecat.flag.paywallComponents=true' >> local.properties - run: name: Run Tests command: java --version && echo $JAVA_HOME && ./gradlew lint test From 9c60790207a48cf739a56ad7d53a03fc6e869507 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Wed, 22 Jan 2025 11:12:16 +0100 Subject: [PATCH 13/16] Revert "Actually fixes test artifact upload on CI." This reverts commit 00998447f0225432e93f580a0e48bb379f40bf80. --- .circleci/config.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 79a8ce2235..7238eea046 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -189,7 +189,6 @@ jobs: command: | mkdir -p build/reports/ find . -type f -path "*/build/reports/*" | grep -v 'paparazzi' | xargs -I {} cp --parents {} build/reports/ - when: always - run: name: Kover HTML command: ./gradlew purchases:koverHtmlReportDefaultsRelease From 98ccae55518991b4747f2177b364aff86271336a Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Wed, 22 Jan 2025 11:12:16 +0100 Subject: [PATCH 14/16] Revert "Fixes test artifact upload on CI." This reverts commit 1e5fd646ae6f871e7c9f61d4d16122d32b05a9f6. --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 7238eea046..bd6350be36 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -187,8 +187,8 @@ jobs: - run: name: Consolidate artifacts command: | - mkdir -p build/reports/ - find . -type f -path "*/build/reports/*" | grep -v 'paparazzi' | xargs -I {} cp --parents {} build/reports/ + mkdir -p build/test-results/ + find . -type f -regex ".*/build/test-results/.*xml" -exec cp --parents {} build/test-results/ \; - run: name: Kover HTML command: ./gradlew purchases:koverHtmlReportDefaultsRelease From ff532920ed68bd806c58f6f840a37b8a90900ed8 Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Wed, 22 Jan 2025 11:13:10 +0100 Subject: [PATCH 15/16] Revert "Some more temporary CI debug options." This reverts commit d7b1feeaf8b2d22c480e4440b8d85a9e0ffda3be. --- .../test/java/com/revenuecat/purchases/OfferingsTest.kt | 8 -------- 1 file changed, 8 deletions(-) diff --git a/purchases/src/test/java/com/revenuecat/purchases/OfferingsTest.kt b/purchases/src/test/java/com/revenuecat/purchases/OfferingsTest.kt index 75c142ba89..e98ef572dd 100644 --- a/purchases/src/test/java/com/revenuecat/purchases/OfferingsTest.kt +++ b/purchases/src/test/java/com/revenuecat/purchases/OfferingsTest.kt @@ -575,22 +575,14 @@ class OfferingsTest { val offeringJson = getOfferingJSON(paywallComponents = getPaywallComponentsDataJson()) val offeringsJson = getOfferingsJSON(offerings = JSONArray(listOf(offeringJson)), uiConfig = uiConfigJson) - println("DEBUGGING - offeringsJson") - offeringsJson.toString(2).lines().forEach { println(it) } - // Act val offerings = offeringsParser.createOfferings(offeringsJson, products) - println("DEBUGGING - offerings") - offerings.toString().chunked(200).forEach { println(it) } // Assert assertThat(offerings).isNotNull assertThat(offerings.all.size).isEqualTo(1) val offering = offerings.all.values.first() - println("DEBUGGING - offering") - offerings.toString().chunked(200).forEach { println(it) } - val paywallComponents = offering.paywallComponents ?: fail("paywallComponents is null") val uiConfig = paywallComponents.uiConfig val colorInfo = uiConfig.app.colors[ColorAlias("primary")]!!.light as ColorInfo.Hex From 170ba1e42e286a0556cb117ded696f1b35f3df5b Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Wed, 22 Jan 2025 11:13:10 +0100 Subject: [PATCH 16/16] Revert "Some temporary CI debug options." This reverts commit e03d92e04be34612478578d534b12477dc39c597. --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index bd6350be36..23754e1350 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -183,7 +183,7 @@ jobs: command: echo 'revenuecat.flag.paywallComponents=true' >> local.properties - run: name: Run Tests - command: java --version && echo $JAVA_HOME && ./gradlew lint test + command: ./gradlew lint test - run: name: Consolidate artifacts command: |