diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/Offering.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/Offering.kt index e178e80bdb..06e99b64ea 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/Offering.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/Offering.kt @@ -5,6 +5,8 @@ package com.revenuecat.purchases +import com.revenuecat.purchases.paywalls.PaywallData + /** * An offering is a collection of [Package] available for the user to purchase. * For more info see https://docs.revenuecat.com/docs/entitlements @@ -18,6 +20,7 @@ data class Offering constructor( val serverDescription: String, val metadata: Map, val availablePackages: List, + val paywall: PaywallData?, ) { /** 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 e3ffc27821..c527c5a648 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/common/OfferingParser.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/common/OfferingParser.kt @@ -6,12 +6,20 @@ import com.revenuecat.purchases.Offerings import com.revenuecat.purchases.Package import com.revenuecat.purchases.PackageType import com.revenuecat.purchases.models.StoreProduct +import com.revenuecat.purchases.paywalls.PaywallData import com.revenuecat.purchases.strings.OfferingStrings import com.revenuecat.purchases.utils.toMap +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json import org.json.JSONObject internal abstract class OfferingParser { + // TODO-PAYWALLS: uncomment after testing + private val json = Json { +// ignoreUnknownKeys = true + } + protected abstract fun findMatchingProduct( productsById: Map>, packageJson: JSONObject, @@ -53,8 +61,20 @@ internal abstract class OfferingParser { } } + val paywallDataJson = offeringJson.optJSONObject("paywall") + + val paywallData = paywallDataJson?.let { + json.decodeFromString(it.toString()) + } + return if (availablePackages.isNotEmpty()) { - Offering(offeringIdentifier, offeringJson.getString("description"), metadata, availablePackages) + Offering( + offeringIdentifier, + offeringJson.getString("description"), + metadata, + availablePackages, + paywallData, + ) } else { null } diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/PaywallColor.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/PaywallColor.kt index ebdfada651..e22443969a 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/PaywallColor.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/PaywallColor.kt @@ -3,6 +3,12 @@ package com.revenuecat.purchases.paywalls import android.graphics.Color import android.os.Build import androidx.annotation.RequiresApi +import kotlinx.serialization.KSerializer +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 /** * Represents a color to be used by `RevenueCatUI`. @@ -18,13 +24,28 @@ data class PaywallColor( @RequiresApi(Build.VERSION_CODES.O) val underlyingColor: Color?, ) { + object Serializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("PaywallColor", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): PaywallColor { + return PaywallColor(decoder.decodeString()) + } + + override fun serialize(encoder: Encoder, value: PaywallColor) { + encoder.encodeString(value.toString()) + } + } + /** * Creates a color from a Hex string: `#RRGGBB` or `#RRGGBBAA`. */ - @RequiresApi(Build.VERSION_CODES.O) constructor(stringRepresentation: String) : this( stringRepresentation, - Color.valueOf(Color.parseColor(stringRepresentation)), + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + Color.valueOf(Color.parseColor(stringRepresentation)) + } else { + null + }, ) } 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 38242fdbe5..6d4e03b653 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/PaywallData.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/PaywallData.kt @@ -1,5 +1,7 @@ package com.revenuecat.purchases.paywalls +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import java.net.URL /** @@ -8,11 +10,12 @@ import java.net.URL * * @see [Paywalls Documentation](https://rev.cat/paywalls) */ +@Serializable data class PaywallData( /** * The type of template used to display this paywall. */ - val templateName: String, + @SerialName("template_name") val templateName: String, /** * Generic configuration for any paywall. @@ -22,12 +25,15 @@ data class PaywallData( /** * The base remote URL where assets for this paywall are stored. */ - val assetBaseURL: URL, - internal val localization: Map, + @SerialName("asset_base_url") @Serializable(with = URLSerializer::class) val assetBaseURL: URL, + // TODO-PAYWALLS: do we need this one? + @SerialName("default_locale") val defaultLocale: String, + @SerialName("localized_strings") internal val localization: Map, ) { /** * Generic configuration for any paywall. */ + @Serializable data class Configuration( /** * The list of package identifiers this paywall will display. @@ -37,7 +43,7 @@ data class PaywallData( /** * The package to be selected by default. */ - val defaultPackage: String? = null, + @SerialName("default_package") val defaultPackage: String? = null, /** * The images for this template. @@ -47,28 +53,29 @@ data class PaywallData( /** * Whether the background image will be blurred (in templates with one). */ - val blurredBackgroundImage: Boolean = false, + @SerialName("blurred_background_image") val blurredBackgroundImage: Boolean = false, /** * Whether a restore purchases button should be displayed. */ - val displayRestorePurchases: Boolean = true, + @SerialName("display_restore_purchases") val displayRestorePurchases: Boolean = true, /** * If set, the paywall will display a terms of service link. */ - val termsOfServiceURL: URL? = null, + @SerialName("tos_url") @Serializable(with = URLSerializer::class) val termsOfServiceURL: URL? = null, /** * If set, the paywall will display a privacy policy link. */ - val privacyURL: URL? = null, + @SerialName("privacy_url") @Serializable(with = URLSerializer::class) val privacyURL: URL? = null, /** * The set of colors used. */ val colors: ColorInformation, ) { + @Serializable data class Images( /** * Image displayed as a header in a template. @@ -86,6 +93,7 @@ data class PaywallData( val icon: String? = null, ) + @Serializable data class ColorInformation( /** * Set of colors for light mode. @@ -98,47 +106,55 @@ data class PaywallData( val dark: Colors? = null, ) + @Serializable data class Colors( /** * Color for the background of the paywall. */ - val background: PaywallColor, + @Serializable(with = PaywallColor.Serializer::class) val background: PaywallColor, /** * Color for the primary text element. */ - val text1: PaywallColor, + @SerialName("text_1") + @Serializable(with = PaywallColor.Serializer::class) val text1: PaywallColor, /** * Color for secondary text element. */ - val text2: PaywallColor? = null, + @SerialName("text_2") + @Serializable(with = PaywallColor.Serializer::class) val text2: PaywallColor? = null, /** * Background color of the main call to action button. */ - val callToActionBackground: PaywallColor, + @SerialName("call_to_action_background") + @Serializable(with = PaywallColor.Serializer::class) val callToActionBackground: PaywallColor, /** * Foreground color of the main call to action button. */ - val callToActionForeground: PaywallColor, + @SerialName("call_to_action_foreground") + @Serializable(with = PaywallColor.Serializer::class) val callToActionForeground: PaywallColor, /** * Primary accent color. */ - val accent1: PaywallColor? = null, + @SerialName("accent_1") + @Serializable(with = PaywallColor.Serializer::class) val accent1: PaywallColor? = null, /** * Secondary accent color. */ - val accent2: PaywallColor? = null, + @SerialName("accent_2") + @Serializable(with = PaywallColor.Serializer::class) val accent2: PaywallColor? = null, ) } /** * Defines the necessary localized information for a paywall. */ + @Serializable data class LocalizedConfiguration( /** * The title of the paywall screen. @@ -148,43 +164,44 @@ data class PaywallData( /** * The subtitle of the paywall screen. */ - val subtitle: String?, + val subtitle: String? = null, /** * The content of the main action button for purchasing a subscription. */ - val callToAction: String, + @SerialName("call_to_action") val callToAction: String, /** * The content of the main action button for purchasing a subscription when an intro offer is available. * If `null`, no information regarding trial eligibility will be displayed. */ - val callToActionWithIntroOffer: String?, + @SerialName("call_to_action_with_intro_offer") val callToActionWithIntroOffer: String? = null, /** * Description for the offer to be purchased. */ - val offerDetails: String?, + @SerialName("offer_details") val offerDetails: String?, /** * Description for the offer to be purchased when an intro offer is available. * If `null`, no information regarding trial eligibility will be displayed. */ - val offerDetailsWithIntroOffer: String?, + @SerialName("offer_details_with_intro_offer") val offerDetailsWithIntroOffer: String? = null, /** * The name representing each of the packages, most commonly a variable. */ - val offerName: String?, + @SerialName("offer_name") val offerName: String? = null, /** * An optional list of features that describe this paywall. */ - val features: List, + val features: List = emptyList(), ) { /** * An item to be showcased in a paywall. */ + @Serializable data class Feature( /** * The title of the feature. @@ -200,7 +217,7 @@ data class PaywallData( * An optional icon for the feature. * This must be an icon identifier known by `RevenueCatUI`. */ - val iconID: String? = null, + @SerialName("icon_id") val iconID: String? = null, ) } } diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/URLSerializer.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/URLSerializer.kt new file mode 100644 index 0000000000..bfce093ec1 --- /dev/null +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/URLSerializer.kt @@ -0,0 +1,20 @@ +package com.revenuecat.purchases.paywalls + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import java.net.URL + +object URLSerializer : KSerializer { + override val descriptor = PrimitiveSerialDescriptor("URL", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): URL { + return URL(decoder.decodeString()) + } + + override fun serialize(encoder: Encoder, value: URL) { + encoder.encodeString(value.toString()) + } +} diff --git a/ui/debugview/src/main/kotlin/com/revenuecat/purchases/ui/debugview/models/TestModels.kt b/ui/debugview/src/main/kotlin/com/revenuecat/purchases/ui/debugview/models/TestModels.kt index 9e859b6c9e..8809482e88 100644 --- a/ui/debugview/src/main/kotlin/com/revenuecat/purchases/ui/debugview/models/TestModels.kt +++ b/ui/debugview/src/main/kotlin/com/revenuecat/purchases/ui/debugview/models/TestModels.kt @@ -86,5 +86,6 @@ internal val testOffering: Offering availablePackages = listOf( Package("package_id", PackageType.ANNUAL, storeProduct, "offering_id"), ), + paywall = null, ) }