Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
@@ -1,6 +1,7 @@
package com.superwall.sdk.analytics.internal.trackable

import com.superwall.sdk.analytics.superwall.SuperwallEvent
import com.superwall.sdk.paywall.view.webview.messaging.PageViewData
Comment on lines 1 to +4
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Import ordering breaks ktlint grouping

PageViewData is inserted between two analytics.superwall imports, splitting what should be a contiguous alphabetical group. This will likely fail ktlint. The same issue occurs in SuperwallEvent.kt (lines 8-9), where paywall.view.webview.messaging.PageViewData precedes paywall.presentation.internal.* imports out of alphabetical order.

Suggested change
package com.superwall.sdk.analytics.internal.trackable
import com.superwall.sdk.analytics.superwall.SuperwallEvent
import com.superwall.sdk.paywall.view.webview.messaging.PageViewData
package com.superwall.sdk.analytics.internal.trackable
import com.superwall.sdk.analytics.superwall.SuperwallEvent
import com.superwall.sdk.analytics.superwall.TransactionProduct
import com.superwall.sdk.paywall.view.webview.messaging.PageViewData

In SuperwallEvent.kt, move the PageViewData import after the PaywallInfo import and before PaywallPresentationRequestStatus to maintain alphabetical ordering within the paywall.* group.

Prompt To Fix With AI
This is a comment left during a code review.
Path: superwall/src/main/java/com/superwall/sdk/analytics/internal/trackable/TrackableSuperwallEvent.kt
Line: 1-4

Comment:
**Import ordering breaks ktlint grouping**

`PageViewData` is inserted between two `analytics.superwall` imports, splitting what should be a contiguous alphabetical group. This will likely fail `ktlint`. The same issue occurs in `SuperwallEvent.kt` (lines 8-9), where `paywall.view.webview.messaging.PageViewData` precedes `paywall.presentation.internal.*` imports out of alphabetical order.

```suggestion
package com.superwall.sdk.analytics.internal.trackable

import com.superwall.sdk.analytics.superwall.SuperwallEvent
import com.superwall.sdk.analytics.superwall.TransactionProduct
import com.superwall.sdk.paywall.view.webview.messaging.PageViewData
```

In `SuperwallEvent.kt`, move the `PageViewData` import after the `PaywallInfo` import and before `PaywallPresentationRequestStatus` to maintain alphabetical ordering within the `paywall.*` group.

How can I resolve this? If you propose a fix, please make it concise.

import com.superwall.sdk.analytics.superwall.TransactionProduct
import com.superwall.sdk.config.models.Survey
import com.superwall.sdk.config.models.SurveyOption
Expand Down Expand Up @@ -494,6 +495,27 @@ sealed class InternalSuperwallEvent(
}
}

class PaywallPageView(
val paywallInfo: PaywallInfo,
val data: PageViewData,
) : InternalSuperwallEvent(SuperwallEvent.PaywallPageView(paywallInfo, data)) {
override val audienceFilterParams: Map<String, Any>
get() = paywallInfo.audienceFilterParams()

override suspend fun getSuperwallParameters(): HashMap<String, Any> {
val params = HashMap(paywallInfo.eventParams())
params["page_node_id"] = data.pageNodeId
params["flow_position"] = data.flowPosition
params["page_name"] = data.pageName
params["navigation_node_id"] = data.navigationNodeId
params["navigation_type"] = data.navigationType
data.previousPageNodeId?.let { params["previous_page_node_id"] = it }
data.previousFlowPosition?.let { params["previous_flow_position"] = it }
data.timeOnPreviousPageMs?.let { params["time_on_previous_page_ms"] = it }
return params
}
}

class Transaction(
val state: State,
val paywallInfo: PaywallInfo,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.superwall.sdk.config.models.SurveyOption
import com.superwall.sdk.models.customer.CustomerInfo
import com.superwall.sdk.models.triggers.TriggerResult
import com.superwall.sdk.paywall.presentation.PaywallInfo
import com.superwall.sdk.paywall.view.webview.messaging.PageViewData
import com.superwall.sdk.paywall.presentation.internal.PaywallPresentationRequestStatus
import com.superwall.sdk.paywall.presentation.internal.PaywallPresentationRequestStatusReason
import com.superwall.sdk.paywall.view.webview.WebviewError
Expand Down Expand Up @@ -138,6 +139,15 @@ sealed class SuperwallEvent {
get() = "paywall_open"
}

// / When a page view occurs in a multi-page paywall.
data class PaywallPageView(
val paywallInfo: PaywallInfo,
val data: PageViewData,
) : SuperwallEvent() {
override val rawName: String
get() = SuperwallEvents.PaywallPageView.rawName
}
Comment on lines +143 to +149
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 PageViewData is a public API type from an internal package

SuperwallEvent.PaywallPageView is part of the SDK's public surface, but its data parameter is typed as PageViewData from com.superwall.sdk.paywall.view.webview.messaging — an implementation-detail package. SDK consumers using this event via SuperwallDelegate will need to import from the webview messaging internals.

Consider moving PageViewData to a more appropriate location (e.g. alongside PaywallInfo in paywall.presentation) or to a dedicated model package so the public API doesn't expose an internal layer.

Prompt To Fix With AI
This is a comment left during a code review.
Path: superwall/src/main/java/com/superwall/sdk/analytics/superwall/SuperwallEvent.kt
Line: 143-149

Comment:
**`PageViewData` is a public API type from an internal package**

`SuperwallEvent.PaywallPageView` is part of the SDK's public surface, but its `data` parameter is typed as `PageViewData` from `com.superwall.sdk.paywall.view.webview.messaging` — an implementation-detail package. SDK consumers using this event via `SuperwallDelegate` will need to import from the webview messaging internals.

Consider moving `PageViewData` to a more appropriate location (e.g. alongside `PaywallInfo` in `paywall.presentation`) or to a dedicated model package so the public API doesn't expose an internal layer.

How can I resolve this? If you propose a fix, please make it concise.


// / When a paywall is closed.
data class PaywallClose(
val paywallInfo: PaywallInfo,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ enum class SuperwallEvents(
PermissionDenied("permission_denied"),
PaywallPreloadStart("paywallPreload_start"),
PaywallPreloadComplete("paywallPreload_complete"),
PaywallPageView("paywall_page_view"),
TestModeModalOpen("testModeModal_open"),
TestModeModalClose("testModeModal_close"),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.superwall.sdk.paywall.view.webview.messaging

data class PageViewData(
val pageNodeId: String,
val flowPosition: Int,
val pageName: String,
val navigationNodeId: String,
val previousPageNodeId: String?,
val previousFlowPosition: Int?,
val navigationType: String,
val timeOnPreviousPageMs: Int?,
)
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.booleanOrNull
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.int
import kotlinx.serialization.json.intOrNull
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
Expand Down Expand Up @@ -129,6 +130,10 @@ sealed class PaywallMessage {
val variables: Map<String, Any>?,
) : PaywallMessage()

data class PageView(
val data: PageViewData,
) : PaywallMessage()

data class HapticFeedback(
val hapticType: HapticType,
) : PaywallMessage() {
Expand Down Expand Up @@ -256,6 +261,20 @@ private fun parsePaywallMessage(json: JsonObject): PaywallMessage {
)
}

"page_view" ->
PaywallMessage.PageView(
PageViewData(
pageNodeId = json["page_node_id"]!!.jsonPrimitive.content,
flowPosition = json["flow_position"]!!.jsonPrimitive.int,
pageName = json["page_name"]!!.jsonPrimitive.content,
navigationNodeId = json["navigation_node_id"]!!.jsonPrimitive.content,
previousPageNodeId = json["previous_page_node_id"]?.jsonPrimitive?.contentOrNull,
previousFlowPosition = json["previous_flow_position"]?.jsonPrimitive?.intOrNull,
navigationType = json["type"]!!.jsonPrimitive.content,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 JSON key "type" is inconsistent with field name navigationType

Every other field in PageViewData maps directly from a snake_case JSON key that matches its Kotlin name (e.g. navigation_node_idnavigationNodeId), but navigationType maps from the ambiguous generic key "type". This deviates from the established convention and is easy to misread.

If the upstream JS contract really sends "type", consider documenting this explicitly, or (if the contract can be changed) align it to "navigation_type".

Suggested change
navigationType = json["type"]!!.jsonPrimitive.content,
navigationType = json["navigation_type"]!!.jsonPrimitive.content,
Prompt To Fix With AI
This is a comment left during a code review.
Path: superwall/src/main/java/com/superwall/sdk/paywall/view/webview/messaging/PaywallMessage.kt
Line: 273

Comment:
**JSON key `"type"` is inconsistent with field name `navigationType`**

Every other field in `PageViewData` maps directly from a snake_case JSON key that matches its Kotlin name (e.g. `navigation_node_id``navigationNodeId`), but `navigationType` maps from the ambiguous generic key `"type"`. This deviates from the established convention and is easy to misread.

If the upstream JS contract really sends `"type"`, consider documenting this explicitly, or (if the contract can be changed) align it to `"navigation_type"`.

```suggestion
                    navigationType = json["navigation_type"]!!.jsonPrimitive.content,
```

How can I resolve this? If you propose a fix, please make it concise.

timeOnPreviousPageMs = json["time_on_previous_page_ms"]?.jsonPrimitive?.intOrNull,
),
)

"haptic_feedback" -> {
val style =
json["haptic_type"]?.jsonPrimitive?.contentOrNull?.let {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,18 @@ class PaywallMessageHandler(

is PaywallMessage.HapticFeedback -> triggerHapticFeedback(message.hapticType)

is PaywallMessage.PageView -> {
val paywallInfo = messageHandler?.state?.info ?: return
ioScope.launch {
track(
InternalSuperwallEvent.PaywallPageView(
paywallInfo = paywallInfo,
data = message.data,
),
)
}
}

else -> {
Logger.debug(
LogLevel.error,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -995,6 +995,94 @@ class InternalSuperwallEventTest {
return StoreTransaction(transaction, configRequestId = "config", appSessionId = "session")
}

@Test
fun paywallPageView_allFieldsMappedToSnakeCase() =
runTest {
Given("a PaywallPageView event with all fields") {
val paywallInfo = stubPaywallInfo()
val data =
com.superwall.sdk.paywall.view.webview.messaging.PageViewData(
pageNodeId = "node-123",
flowPosition = 2,
pageName = "Pricing",
navigationNodeId = "nav-456",
previousPageNodeId = "node-000",
previousFlowPosition = 1,
navigationType = "forward",
timeOnPreviousPageMs = 3500,
)
val event =
InternalSuperwallEvent.PaywallPageView(
paywallInfo = paywallInfo,
data = data,
)

When("parameters are requested") {
val params = event.getSuperwallParameters()

Then("all page view fields are mapped to snake_case keys") {
assertEquals("node-123", params["page_node_id"])
assertEquals(2, params["flow_position"])
assertEquals("Pricing", params["page_name"])
assertEquals("nav-456", params["navigation_node_id"])
assertEquals("forward", params["navigation_type"])
assertEquals("node-000", params["previous_page_node_id"])
assertEquals(1, params["previous_flow_position"])
assertEquals(3500, params["time_on_previous_page_ms"])
}

And("paywall info params are also included") {
assertEquals(paywallInfo.identifier, params["paywall_identifier"])
}

And("the superwall placement is paywall_page_view") {
assertEquals("paywall_page_view", event.superwallPlacement.rawName)
}
}
}
}

@Test
fun paywallPageView_optionalFieldsOmitted() =
runTest {
Given("a PaywallPageView event without optional fields") {
val paywallInfo = stubPaywallInfo()
val data =
com.superwall.sdk.paywall.view.webview.messaging.PageViewData(
pageNodeId = "node-first",
flowPosition = 0,
pageName = "Welcome",
navigationNodeId = "nav-001",
previousPageNodeId = null,
previousFlowPosition = null,
navigationType = "entry",
timeOnPreviousPageMs = null,
)
val event =
InternalSuperwallEvent.PaywallPageView(
paywallInfo = paywallInfo,
data = data,
)

When("parameters are requested") {
val params = event.getSuperwallParameters()

Then("required fields are present") {
assertEquals("node-first", params["page_node_id"])
assertEquals(0, params["flow_position"])
assertEquals("Welcome", params["page_name"])
assertEquals("entry", params["navigation_type"])
}

And("optional fields are absent") {
assertFalse(params.containsKey("previous_page_node_id"))
assertFalse(params.containsKey("previous_flow_position"))
assertFalse(params.containsKey("time_on_previous_page_ms"))
}
}
}
}

private object NoopPresentationFactory : InternalSuperwallEvent.PresentationRequest.Factory {
override suspend fun makeRuleAttributes(
event: EventData?,
Expand Down
Loading
Loading