Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,44 @@ internal sealed class BackendEvent : Event {
val errorCode: Int? = null,
@SerialName("error_message")
val errorMessage: String? = null,
@SerialName("component_type")
val componentType: String? = null,
@SerialName("component_name")
val componentName: String? = null,
@SerialName("component_value")
val componentValue: String? = null,
@SerialName("component_url")
val componentUrl: String? = null,
@SerialName("origin_index")
val originIndex: Int? = null,
@SerialName("destination_index")
val destinationIndex: Int? = null,
@SerialName("origin_context_name")
val originContextName: String? = null,
@SerialName("destination_context_name")
val destinationContextName: String? = null,
@SerialName("default_index")
val defaultIndex: Int? = null,
@SerialName("origin_package_id")
val originPackageIdentifier: String? = null,
@SerialName("destination_package_id")
val destinationPackageIdentifier: String? = null,
@SerialName("default_package_id")
val defaultPackageIdentifier: String? = null,
@SerialName("origin_product_id")
val originProductIdentifier: String? = null,
@SerialName("destination_product_id")
val destinationProductIdentifier: String? = null,
@SerialName("default_product_id")
val defaultProductIdentifier: String? = null,
@SerialName("current_package_id")
val currentPackageIdentifier: String? = null,
@SerialName("resulting_package_id")
val resultingPackageIdentifier: String? = null,
@SerialName("current_product_id")
val currentProductIdentifier: String? = null,
@SerialName("resulting_product_id")
val resultingProductIdentifier: String? = null,
) : BackendEvent()

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.revenuecat.purchases.customercenter.events.CustomerCenterImpressionEv
import com.revenuecat.purchases.customercenter.events.CustomerCenterSurveyOptionChosenEvent
import com.revenuecat.purchases.paywalls.events.CustomPaywallEvent
import com.revenuecat.purchases.paywalls.events.PaywallEvent
import com.revenuecat.purchases.paywalls.events.toBackendComponentFields
import com.revenuecat.purchases.paywalls.events.PaywallEventType
import com.revenuecat.purchases.utils.Event
import kotlinx.serialization.SerialName
Expand Down Expand Up @@ -86,6 +87,7 @@ internal fun PaywallEvent.toBackendStoredEvent(
// WIP: We should implement support for these events in the backend.
return null
}
val backendComponentFields = componentInteraction.toBackendComponentFields()
return BackendStoredEvent.Paywalls(
BackendEvent.Paywalls(
id = creationData.id.toString(),
Expand All @@ -106,6 +108,25 @@ internal fun PaywallEvent.toBackendStoredEvent(
productID = data.productIdentifier,
errorCode = data.errorCode,
errorMessage = data.errorMessage,
componentType = backendComponentFields.componentType,
componentName = backendComponentFields.componentName,
componentValue = backendComponentFields.componentValue,
componentUrl = backendComponentFields.componentUrl,
originIndex = backendComponentFields.originIndex,
destinationIndex = backendComponentFields.destinationIndex,
originContextName = backendComponentFields.originContextName,
destinationContextName = backendComponentFields.destinationContextName,
defaultIndex = backendComponentFields.defaultIndex,
originPackageIdentifier = backendComponentFields.originPackageIdentifier,
destinationPackageIdentifier = backendComponentFields.destinationPackageIdentifier,
defaultPackageIdentifier = backendComponentFields.defaultPackageIdentifier,
originProductIdentifier = backendComponentFields.originProductIdentifier,
destinationProductIdentifier = backendComponentFields.destinationProductIdentifier,
defaultProductIdentifier = backendComponentFields.defaultProductIdentifier,
currentPackageIdentifier = backendComponentFields.currentPackageIdentifier,
resultingPackageIdentifier = backendComponentFields.resultingPackageIdentifier,
currentProductIdentifier = backendComponentFields.currentProductIdentifier,
resultingProductIdentifier = backendComponentFields.resultingProductIdentifier,
),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public class ButtonComponent(
@get:JvmSynthetic public val action: Action,
@get:JvmSynthetic public val stack: StackComponent,
@get:JvmSynthetic public val transition: PaywallTransition? = null,
@get:JvmSynthetic public val name: String? = null,
) : PaywallComponent {

@InternalRevenueCatAPI
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ public class CarouselComponent(
public val autoAdvance: AutoAdvancePages? = null,
@get:JvmSynthetic
public val overrides: List<ComponentOverride<PartialCarouselComponent>> = emptyList(),
@get:JvmSynthetic
public val name: String? = null,
) : PaywallComponent {

@Poko
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,7 @@ public class PackageComponent(
@Serializable(with = ResilientPromoOfferConfigSerializer::class)
@SerialName("play_store_offer")
public val playStoreOffer: PromoOfferConfig? = null,
@get:JvmSynthetic
@SerialName("name")
public val name: String? = null,
) : PaywallComponent
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public class TabControlButtonComponent(
public val tabId: String,
@get:JvmSynthetic
public val stack: StackComponent,
@get:JvmSynthetic
public val name: String? = null,
) : PaywallComponent

@InternalRevenueCatAPI
Expand All @@ -43,6 +45,8 @@ public class TabControlToggleComponent(
@SerialName("default_value")
public val defaultValue: Boolean,
@get:JvmSynthetic
public val name: String? = null,
@get:JvmSynthetic
@SerialName("thumb_color_on")
public val thumbColorOn: ColorScheme,
@get:JvmSynthetic
Expand All @@ -67,6 +71,8 @@ public object TabControlComponent : PaywallComponent
@SerialName("tabs")
@Immutable
public class TabsComponent(
@get:JvmSynthetic
public val name: String? = null,
@get:JvmSynthetic
public val visible: Boolean? = null,
@get:JvmSynthetic
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ constructor(
public val margin: Padding = zero,
@get:JvmSynthetic
public val overrides: List<ComponentOverride<PartialTextComponent>> = emptyList(),
@get:JvmSynthetic
public val name: String? = null,
) : PaywallComponent

@InternalRevenueCatAPI
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.revenuecat.purchases.common.events.FeatureEvent
import com.revenuecat.purchases.utils.serializers.DateSerializer
import com.revenuecat.purchases.utils.serializers.UUIDSerializer
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.builtins.nullable
Expand All @@ -17,6 +18,7 @@ import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.encoding.encodeStructure
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonNames
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import java.util.Date
Expand Down Expand Up @@ -57,8 +59,68 @@ public enum class PaywallEventType(public val value: String) {
* An exit offer will be shown to the user.
*/
EXIT_OFFER("paywall_exit_offer"),

/**
* User interacted with a paywall component (tabs, carousel, non-purchase button, etc.).
*/
COMPONENT_INTERACTION("paywall_component_interaction"),
}

/**
* Component categories for [PaywallEventType.COMPONENT_INTERACTION]. Wire values match iOS `ControlType`.
*/
@InternalRevenueCatAPI
@Serializable
public enum class PaywallComponentType {
@SerialName("tab")
TAB,

@SerialName("switch")
SWITCH,

@SerialName("carousel")
CAROUSEL,

@SerialName("button")
BUTTON,

@SerialName("text")
TEXT,

@SerialName("package")
PACKAGE,

@SerialName("package_selection_sheet")
PACKAGE_SELECTION_SHEET,
}

/**
* Payload for [PaywallEventType.COMPONENT_INTERACTION].
*/
@InternalRevenueCatAPI
@Serializable
public data class PaywallComponentInteractionData(
public val componentType: PaywallComponentType,
public val componentName: String? = null,
public val componentValue: String,
public val componentUrl: String? = null,
public val originIndex: Int? = null,
public val destinationIndex: Int? = null,
public val originContextName: String? = null,
public val destinationContextName: String? = null,
public val defaultIndex: Int? = null,
public val originPackageIdentifier: String? = null,
public val destinationPackageIdentifier: String? = null,
public val defaultPackageIdentifier: String? = null,
public val originProductIdentifier: String? = null,
public val destinationProductIdentifier: String? = null,
public val defaultProductIdentifier: String? = null,
public val currentPackageIdentifier: String? = null,
public val resultingPackageIdentifier: String? = null,
public val currentProductIdentifier: String? = null,
public val resultingProductIdentifier: String? = null,
)

/**
* Types of exit offers. Meant for RevenueCatUI use.
*/
Expand All @@ -80,6 +142,8 @@ public data class PaywallEvent(
public val creationData: CreationData,
public val data: Data,
public val type: PaywallEventType,
@JsonNames("controlInteraction")
public val componentInteraction: PaywallComponentInteractionData? = null,
) : FeatureEvent {

override val isPriorityEvent: Boolean get() = type == PaywallEventType.IMPRESSION
Expand Down Expand Up @@ -281,3 +345,66 @@ internal object PaywallEventDataSerializer : KSerializer<PaywallEvent.Data> {
)
}
}

@InternalRevenueCatAPI
internal fun PaywallComponentType.toWireString(): String = when (this) {
PaywallComponentType.TAB -> "tab"
PaywallComponentType.SWITCH -> "switch"
PaywallComponentType.CAROUSEL -> "carousel"
PaywallComponentType.BUTTON -> "button"
PaywallComponentType.TEXT -> "text"
PaywallComponentType.PACKAGE -> "package"
PaywallComponentType.PACKAGE_SELECTION_SHEET -> "package_selection_sheet"
}

/**
* Flattened component-interaction values for [BackendEvent.Paywalls] (shared by stored-event paths).
*/
@InternalRevenueCatAPI
internal data class BackendPaywallComponentFields(
val componentType: String? = null,
val componentName: String? = null,
val componentValue: String? = null,
val componentUrl: String? = null,
val originIndex: Int? = null,
val destinationIndex: Int? = null,
val originContextName: String? = null,
val destinationContextName: String? = null,
val defaultIndex: Int? = null,
val originPackageIdentifier: String? = null,
val destinationPackageIdentifier: String? = null,
val defaultPackageIdentifier: String? = null,
val originProductIdentifier: String? = null,
val destinationProductIdentifier: String? = null,
val defaultProductIdentifier: String? = null,
val currentPackageIdentifier: String? = null,
val resultingPackageIdentifier: String? = null,
val currentProductIdentifier: String? = null,
val resultingProductIdentifier: String? = null,
)

@InternalRevenueCatAPI
internal fun PaywallComponentInteractionData?.toBackendComponentFields(): BackendPaywallComponentFields {
val interaction = this ?: return BackendPaywallComponentFields()
return BackendPaywallComponentFields(
componentType = interaction.componentType.toWireString(),
componentName = interaction.componentName,
componentValue = interaction.componentValue,
componentUrl = interaction.componentUrl,
originIndex = interaction.originIndex,
destinationIndex = interaction.destinationIndex,
originContextName = interaction.originContextName,
destinationContextName = interaction.destinationContextName,
defaultIndex = interaction.defaultIndex,
originPackageIdentifier = interaction.originPackageIdentifier,
destinationPackageIdentifier = interaction.destinationPackageIdentifier,
defaultPackageIdentifier = interaction.defaultPackageIdentifier,
originProductIdentifier = interaction.originProductIdentifier,
destinationProductIdentifier = interaction.destinationProductIdentifier,
defaultProductIdentifier = interaction.defaultProductIdentifier,
currentPackageIdentifier = interaction.currentPackageIdentifier,
resultingPackageIdentifier = interaction.resultingPackageIdentifier,
currentProductIdentifier = interaction.currentProductIdentifier,
resultingProductIdentifier = interaction.resultingProductIdentifier,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ internal data class PaywallStoredEvent(

@OptIn(InternalRevenueCatAPI::class)
fun toBackendEvent(): BackendEvent.Paywalls {
val backendComponentFields = event.componentInteraction.toBackendComponentFields()
return BackendEvent.Paywalls(
id = event.creationData.id.toString(),
version = BackendEvent.PAYWALL_EVENT_SCHEMA_VERSION,
Expand All @@ -37,6 +38,29 @@ internal data class PaywallStoredEvent(
localeIdentifier = event.data.localeIdentifier,
exitOfferType = event.data.exitOfferType?.value,
exitOfferingID = event.data.exitOfferingIdentifier,
packageID = event.data.packageIdentifier,
productID = event.data.productIdentifier,
errorCode = event.data.errorCode,
errorMessage = event.data.errorMessage,
componentType = backendComponentFields.componentType,
componentName = backendComponentFields.componentName,
componentValue = backendComponentFields.componentValue,
componentUrl = backendComponentFields.componentUrl,
originIndex = backendComponentFields.originIndex,
destinationIndex = backendComponentFields.destinationIndex,
originContextName = backendComponentFields.originContextName,
destinationContextName = backendComponentFields.destinationContextName,
defaultIndex = backendComponentFields.defaultIndex,
originPackageIdentifier = backendComponentFields.originPackageIdentifier,
destinationPackageIdentifier = backendComponentFields.destinationPackageIdentifier,
defaultPackageIdentifier = backendComponentFields.defaultPackageIdentifier,
originProductIdentifier = backendComponentFields.originProductIdentifier,
destinationProductIdentifier = backendComponentFields.destinationProductIdentifier,
defaultProductIdentifier = backendComponentFields.defaultProductIdentifier,
currentPackageIdentifier = backendComponentFields.currentPackageIdentifier,
resultingPackageIdentifier = backendComponentFields.resultingPackageIdentifier,
currentProductIdentifier = backendComponentFields.currentProductIdentifier,
resultingProductIdentifier = backendComponentFields.resultingProductIdentifier,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,29 @@ internal class PackageComponentTests(@Suppress("UNUSED_PARAMETER") name: String,
)
),
),
arrayOf(
"optional name",
Args(
json = """
{
"type": "package",
"package_id": "${"$"}rc_weekly",
"is_selected_by_default": true,
"stack": {
"type": "stack",
"components": []
},
"name": "hero_package"
}
""".trimIndent(),
expected = PackageComponent(
packageId = "${"$"}rc_weekly",
isSelectedByDefault = true,
stack = StackComponent(components = emptyList()),
name = "hero_package",
)
),
),
)
}

Expand Down
Loading