Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ internal sealed class BackendEvent : Event {
val darkMode: Boolean,
@SerialName("locale")
val localeIdentifier: String,
@SerialName("presented_offering_context")
val presentedOfferingContext: PresentedOfferingContextData? = null,
@SerialName("exit_offer_type")
val exitOfferType: String? = null,
@SerialName("exit_offering_id")
Expand All @@ -113,6 +115,31 @@ internal sealed class BackendEvent : Event {
val errorMessage: String? = null,
) : BackendEvent()

@Serializable
data class PresentedOfferingContextData(
@SerialName("placement_identifier")
val placementIdentifier: String? = null,
@SerialName("targeting_revision")
val targetingRevision: Int? = null,
@SerialName("targeting_rule_id")
val targetingRuleId: String? = null,
) {
companion object {
fun fromContext(
context: com.revenuecat.purchases.PresentedOfferingContext,
): PresentedOfferingContextData? {
if (context.placementIdentifier == null && context.targetingContext == null) {
return null
}
return PresentedOfferingContextData(
placementIdentifier = context.placementIdentifier,
targetingRevision = context.targetingContext?.revision,
targetingRuleId = context.targetingContext?.ruleId,
)
}
}
}

/**
* Represents an event related to a custom paywall.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ internal fun PaywallEvent.toBackendStoredEvent(
displayMode = data.displayMode,
darkMode = data.darkMode,
localeIdentifier = data.localeIdentifier,
presentedOfferingContext = BackendEvent.PresentedOfferingContextData.fromContext(
data.presentedOfferingContext,
),
exitOfferType = data.exitOfferType?.value,
exitOfferingID = data.exitOfferingIdentifier,
packageID = data.packageIdentifier,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ internal data class PaywallStoredEvent(
displayMode = event.data.displayMode,
darkMode = event.data.darkMode,
localeIdentifier = event.data.localeIdentifier,
presentedOfferingContext = BackendEvent.PresentedOfferingContextData.fromContext(
event.data.presentedOfferingContext,
),
exitOfferType = event.data.exitOfferType?.value,
exitOfferingID = event.data.exitOfferingIdentifier,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ import com.revenuecat.purchases.common.Delay
import com.revenuecat.purchases.common.Dispatcher
import com.revenuecat.purchases.common.HTTPClient
import com.revenuecat.purchases.common.SyncDispatcher
import com.revenuecat.purchases.PresentedOfferingContext
import com.revenuecat.purchases.common.events.BackendEvent
import com.revenuecat.purchases.common.events.BackendStoredEvent
import com.revenuecat.purchases.common.events.EventsRequest
import com.revenuecat.purchases.common.events.toBackendEvent
import com.revenuecat.purchases.common.events.toBackendStoredEvent
import com.revenuecat.purchases.paywalls.events.PaywallEvent
import com.revenuecat.purchases.common.networking.Endpoint
import com.revenuecat.purchases.common.networking.HTTPResult
import com.revenuecat.purchases.common.networking.RCHTTPStatusCodes
Expand All @@ -33,6 +36,8 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import java.io.IOException
import java.util.Date
import java.util.UUID
import java.util.concurrent.CountDownLatch
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.ThreadPoolExecutor
Expand Down Expand Up @@ -60,6 +65,30 @@ class BackendPaywallEventTest {
)
).map { it.toBackendEvent() })

private val placementTargetingEventRequest = EventsRequest(listOf(
BackendStoredEvent.Paywalls(
BackendEvent.Paywalls(
id = "placement-id",
version = 1,
type = PaywallEventType.IMPRESSION.value,
appUserID = "appUserID",
sessionID = "sessionID",
offeringID = "offeringID",
paywallID = "paywallID",
paywallRevision = 5,
timestamp = 123456789,
displayMode = "full_screen",
darkMode = true,
localeIdentifier = "es_ES",
presentedOfferingContext = BackendEvent.PresentedOfferingContextData(
placementIdentifier = "home_banner",
targetingRevision = 3,
targetingRuleId = "rule_abc123",
),
)
)
).map { it.toBackendEvent() })

private val exitOfferEventRequest = EventsRequest(listOf(
BackendStoredEvent.Paywalls(
BackendEvent.Paywalls(
Expand Down Expand Up @@ -150,6 +179,44 @@ class BackendPaywallEventTest {
)
}

@Test
fun `postPaywallEvents posts events with placement and targeting correctly`() {
mockHttpResult()
backend.postEvents(
placementTargetingEventRequest,
baseURL = AppConfig.paywallEventsURL,
delay = Delay.DEFAULT,
onSuccessHandler = {},
onErrorHandler = { _, _ -> },
)
verifyCallWithBody(
"{" +
"\"events\":[" +
"{" +
"\"discriminator\":\"paywalls\"," +
"\"id\":\"placement-id\"," +
"\"version\":1," +
"\"type\":\"paywall_impression\"," +
"\"app_user_id\":\"appUserID\"," +
"\"session_id\":\"sessionID\"," +
"\"offering_id\":\"offeringID\"," +
"\"paywall_id\":\"paywallID\"," +
"\"paywall_revision\":5," +
"\"timestamp\":123456789," +
"\"display_mode\":\"full_screen\"," +
"\"dark_mode\":true," +
"\"locale\":\"es_ES\"," +
"\"presented_offering_context\":{" +
"\"placement_identifier\":\"home_banner\"," +
"\"targeting_revision\":3," +
"\"targeting_rule_id\":\"rule_abc123\"" +
"}" +
"}" +
"]" +
"}"
)
}

@Test
fun `postPaywallEvents posts exit offer events correctly`() {
mockHttpResult()
Expand Down Expand Up @@ -341,6 +408,44 @@ class BackendPaywallEventTest {
}
}

@Test
fun `toBackendStoredEvent preserves placement and targeting from PresentedOfferingContext`() {
val paywallEvent = PaywallEvent(
creationData = PaywallEvent.CreationData(
id = UUID.fromString("298207f4-87af-4b57-a581-eb27bcc6e009"),
date = Date(1699270688884)
),
data = PaywallEvent.Data(
paywallIdentifier = "paywallID",
presentedOfferingContext = PresentedOfferingContext(
offeringIdentifier = "offeringID",
placementIdentifier = "home_banner",
targetingContext = PresentedOfferingContext.TargetingContext(
revision = 3,
ruleId = "rule_abc123",
),
),
paywallRevision = 5,
sessionIdentifier = UUID.fromString("315107f4-98bf-4b68-a582-eb27bcb6e111"),
displayMode = "footer",
localeIdentifier = "es_ES",
darkMode = true
),
type = PaywallEventType.IMPRESSION,
)

val storedEvent = paywallEvent.toBackendStoredEvent("testAppUserId")
assertThat(storedEvent).isNotNull
assertThat(storedEvent).isInstanceOf(BackendStoredEvent.Paywalls::class.java)

val backendEvent = (storedEvent as BackendStoredEvent.Paywalls).event
assertThat(backendEvent.offeringID).isEqualTo("offeringID")
assertThat(backendEvent.presentedOfferingContext).isNotNull
assertThat(backendEvent.presentedOfferingContext?.placementIdentifier).isEqualTo("home_banner")
assertThat(backendEvent.presentedOfferingContext?.targetingRevision).isEqualTo(3)
assertThat(backendEvent.presentedOfferingContext?.targetingRuleId).isEqualTo("rule_abc123")
}

private fun verifyCallWithBody(body: String) {
val expectedRequest: EventsRequest = JsonProvider.defaultJson.decodeFromString(body)
val expectedBody = JsonProvider.defaultJson.encodeToJsonElement(expectedRequest).asMap()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,47 @@ class PaywallEventSerializationTests {
assertThat(decodedEvent).isEqualTo(exitOfferEvent)
}

@Test
fun `round trip serialization preserves placement and targeting in backend event`() {
val eventString = PaywallStoredEvent.json.encodeToString(impressionEvent)
val decodedEvent = PaywallStoredEvent.fromString(eventString)
val backendEvent = decodedEvent.toBackendEvent()

val context = backendEvent.presentedOfferingContext
assertThat(context).isNotNull
assertThat(context?.placementIdentifier).isEqualTo("placementID")
assertThat(context?.targetingRevision).isEqualTo(5)
assertThat(context?.targetingRuleId).isEqualTo("ruleID")
}

@Test
fun `round trip serialization without placement produces null backend context`() {
val eventWithoutPlacement = PaywallStoredEvent(
event = PaywallEvent(
creationData = PaywallEvent.CreationData(
id = UUID.fromString("298207f4-87af-4b57-a581-eb27bcc6e009"),
date = Date(1699270688884)
),
data = PaywallEvent.Data(
paywallIdentifier = "paywallID",
presentedOfferingContext = PresentedOfferingContext("offeringID"),
paywallRevision = 5,
sessionIdentifier = UUID.fromString("315107f4-98bf-4b68-a582-eb27bcb6e111"),
displayMode = "footer",
localeIdentifier = "es_ES",
darkMode = true
),
type = PaywallEventType.IMPRESSION,
),
userID = "testAppUserId",
)
val eventString = PaywallStoredEvent.json.encodeToString(eventWithoutPlacement)
val decodedEvent = PaywallStoredEvent.fromString(eventString)
val backendEvent = decodedEvent.toBackendEvent()

assertThat(backendEvent.presentedOfferingContext).isNull()
}

@Test
fun `can decode old cached event with offeringIdentifier string field`() {
// Old format with "offeringIdentifier" as a string field instead of "presentedOfferingContext" object
Expand Down Expand Up @@ -240,6 +281,81 @@ class PaywallEventSerializationTests {
assertThat(eventString).contains("\"presentedOfferingContext\":{\"offeringIdentifier\":\"offeringID\"")
}

@Test
fun `round trip serialization with placement only preserves placement in backend event`() {
val eventWithPlacementOnly = PaywallStoredEvent(
event = PaywallEvent(
creationData = PaywallEvent.CreationData(
id = UUID.fromString("298207f4-87af-4b57-a581-eb27bcc6e009"),
date = Date(1699270688884)
),
data = PaywallEvent.Data(
paywallIdentifier = "paywallID",
presentedOfferingContext = PresentedOfferingContext(
offeringIdentifier = "offeringID",
placementIdentifier = "placementID",
targetingContext = null,
),
paywallRevision = 5,
sessionIdentifier = UUID.fromString("315107f4-98bf-4b68-a582-eb27bcb6e111"),
displayMode = "footer",
localeIdentifier = "es_ES",
darkMode = true
),
type = PaywallEventType.IMPRESSION,
),
userID = "testAppUserId",
)
val eventString = PaywallStoredEvent.json.encodeToString(eventWithPlacementOnly)
val decodedEvent = PaywallStoredEvent.fromString(eventString)
val backendEvent = decodedEvent.toBackendEvent()

val context = backendEvent.presentedOfferingContext
assertThat(context).isNotNull
assertThat(context?.placementIdentifier).isEqualTo("placementID")
assertThat(context?.targetingRevision).isNull()
assertThat(context?.targetingRuleId).isNull()
}

@Test
fun `round trip serialization with targeting only preserves targeting in backend event`() {
val eventWithTargetingOnly = PaywallStoredEvent(
event = PaywallEvent(
creationData = PaywallEvent.CreationData(
id = UUID.fromString("298207f4-87af-4b57-a581-eb27bcc6e009"),
date = Date(1699270688884)
),
data = PaywallEvent.Data(
paywallIdentifier = "paywallID",
presentedOfferingContext = PresentedOfferingContext(
offeringIdentifier = "offeringID",
placementIdentifier = null,
targetingContext = PresentedOfferingContext.TargetingContext(
revision = 7,
ruleId = "targetingRuleID",
),
),
paywallRevision = 5,
sessionIdentifier = UUID.fromString("315107f4-98bf-4b68-a582-eb27bcb6e111"),
displayMode = "footer",
localeIdentifier = "es_ES",
darkMode = true
),
type = PaywallEventType.IMPRESSION,
),
userID = "testAppUserId",
)
val eventString = PaywallStoredEvent.json.encodeToString(eventWithTargetingOnly)
val decodedEvent = PaywallStoredEvent.fromString(eventString)
val backendEvent = decodedEvent.toBackendEvent()

val context = backendEvent.presentedOfferingContext
assertThat(context).isNotNull
assertThat(context?.placementIdentifier).isNull()
assertThat(context?.targetingRevision).isEqualTo(7)
assertThat(context?.targetingRuleId).isEqualTo("targetingRuleID")
}

@Test
fun `old format with purchase error fields can be decoded`() {
val oldFormatJson = """
Expand Down