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 40909a444..85fa94ae1 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/common/OfferingParser.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/common/OfferingParser.kt @@ -15,9 +15,11 @@ import org.json.JSONObject internal abstract class OfferingParser { - // TODO-PAYWALLS: uncomment after testing - private val json = Json { - ignoreUnknownKeys = true + companion object { + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal val json = Json { + ignoreUnknownKeys = true + } } protected abstract fun findMatchingProduct( diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/EmptyStringToNullSerializer.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/EmptyStringToNullSerializer.kt new file mode 100644 index 000000000..c135c0be4 --- /dev/null +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/EmptyStringToNullSerializer.kt @@ -0,0 +1,33 @@ +package com.revenuecat.purchases.paywalls + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.nullable +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +/** + * Decodes empty or blank strings as null + */ +internal object EmptyStringToNullSerializer : KSerializer { + private val delegate = String.serializer().nullable + + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor( + "EmptyStringToNullSerializer", + PrimitiveKind.STRING + ) + + override fun deserialize(decoder: Decoder): String? { + return delegate.deserialize(decoder)?.takeIf(String::isNotBlank) + } + + override fun serialize(encoder: Encoder, value: String?) { + when (value) { + null -> encoder.encodeString("") + else -> encoder.encodeString(value) + } + } +} diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/PaywallData.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/PaywallData.kt index bba575007..5ef82a409 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/PaywallData.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/PaywallData.kt @@ -133,16 +133,19 @@ data class PaywallData( /** * Image displayed as a header in a template. */ + @Serializable(with = EmptyStringToNullSerializer::class) val header: String? = null, /** * Image displayed as a background in a template. */ + @Serializable(with = EmptyStringToNullSerializer::class) val background: String? = null, /** * Image displayed as an app icon in a template. */ + @Serializable(with = EmptyStringToNullSerializer::class) val icon: String? = null, ) { internal val all: List diff --git a/purchases/src/test/java/com/revenuecat/purchases/paywalls/PaywallDataTest.kt b/purchases/src/test/java/com/revenuecat/purchases/paywalls/PaywallDataTest.kt index dcf037161..10bedac4a 100644 --- a/purchases/src/test/java/com/revenuecat/purchases/paywalls/PaywallDataTest.kt +++ b/purchases/src/test/java/com/revenuecat/purchases/paywalls/PaywallDataTest.kt @@ -2,9 +2,9 @@ package com.revenuecat.purchases.paywalls import android.graphics.Color import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.revenuecat.purchases.common.OfferingParser import com.revenuecat.purchases.utils.toLocale import kotlinx.serialization.decodeFromString -import kotlinx.serialization.json.Json import org.assertj.core.api.Assertions.assertThat import org.junit.Test import org.junit.runner.RunWith @@ -15,16 +15,14 @@ import java.util.* private const val PAYWALLDATA_SAMPLE1 = "paywalldata-sample1.json" private const val PAYWALLDATA_MISSING_CURRENT_LOCALE = "paywalldata-missing_current_locale.json" +private const val PAYWALLDATA_EMPTY_IMAGES = "paywalldata-empty-images.json" @RunWith(AndroidJUnit4::class) @Config(manifest = Config.NONE) class PaywallDataTest { - @Test fun `test PaywallData properties`() { - val json = loadJSON(PAYWALLDATA_SAMPLE1) - - val paywall: PaywallData = Json.decodeFromString(json) + val paywall: PaywallData = decode(PAYWALLDATA_SAMPLE1) assertThat(paywall.templateName).isEqualTo("1") assertThat(paywall.assetBaseURL).isEqualTo(URL("https://rc-paywalls.s3.amazonaws.com")) @@ -78,9 +76,7 @@ class PaywallDataTest { @Test fun `finds locale if it only has language`() { - val json = loadJSON(PAYWALLDATA_SAMPLE1) - - val paywall: PaywallData = Json.decodeFromString(json) + val paywall: PaywallData = decode(PAYWALLDATA_SAMPLE1) val enConfig = paywall.configForLocale(Locale("en")) assertThat(enConfig?.title).isEqualTo("Paywall") @@ -91,9 +87,7 @@ class PaywallDataTest { @Test fun `does not return a locale if no matching language`() { - val json = loadJSON(PAYWALLDATA_SAMPLE1) - - val paywall: PaywallData = Json.decodeFromString(json) + val paywall: PaywallData = decode(PAYWALLDATA_SAMPLE1) val enConfig = paywall.configForLocale(Locale("fr")) assertThat(enConfig).isNull() @@ -101,15 +95,23 @@ class PaywallDataTest { @Test fun `if current locale is missing it loads available locale`() { - val json = loadJSON(PAYWALLDATA_MISSING_CURRENT_LOCALE) - - val paywall: PaywallData = Json.decodeFromString(json) + val paywall: PaywallData = decode(PAYWALLDATA_MISSING_CURRENT_LOCALE) val localization = paywall.localizedConfiguration.second assertThat(localization.callToAction).isEqualTo("Comprar") assertThat(localization.title).isEqualTo("Tienda") } + @Test + fun `decodes empty images as null`() { + val paywall: PaywallData = decode(PAYWALLDATA_EMPTY_IMAGES) + + val images = paywall.config.images + assertThat(images.background).isNull() + assertThat(images.header).isNull() + assertThat(images.icon).isNull() + } + @Test fun `paywall color can be created from a ColorInt`() { val colorInt = Color.parseColor("#FFAABB") @@ -121,5 +123,6 @@ class PaywallDataTest { } private fun loadJSON(jsonFileName: String) = File(javaClass.classLoader!!.getResource(jsonFileName).file).readText() - + private fun decode(file: String): PaywallData = + OfferingParser.json.decodeFromString(loadJSON(file)) } diff --git a/purchases/src/test/resources/paywalldata-empty-images.json b/purchases/src/test/resources/paywalldata-empty-images.json new file mode 100644 index 000000000..36bc8a37d --- /dev/null +++ b/purchases/src/test/resources/paywalldata-empty-images.json @@ -0,0 +1,32 @@ +{ + "template_name": "1", + "localized_strings": { + "en_US": { + "title": "Paywall", + "subtitle": "Description", + "call_to_action": "Purchase now", + "call_to_action_with_intro_offer": "Purchase now", + "offer_details": "{{ sub_price_per_month }} per month", + "offer_details_with_intro_offer": "Start your {{ sub_offer_duration }} trial, then {{ sub_price_per_month }} per month" + } + }, + "config": { + "packages": ["$rc_monthly", "$rc_annual"], + "images": { + "header": "", + "background": " ", + "icon": null + }, + "colors": { + "light": { + "background": "#FFFFFF", + "text_1": "#FFFFFF", + "call_to_action_background": "#FFFFFF", + "call_to_action_foreground": "#FFFFFF", + "accent_1": "#FFFFFF" + }, + "dark": null + } + }, + "asset_base_url": "https://rc-paywalls.s3.amazonaws.com" +}