Skip to content
Draft
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
Expand Up @@ -57,6 +57,7 @@ internal class PaywallComponentSerializer : KSerializer<PaywallComponent> {
"tabs" -> jsonDecoder.json.decodeFromJsonElement<TabsComponent>(json)
"video" -> jsonDecoder.json.decodeFromJsonElement<VideoComponent>(json)
"countdown" -> jsonDecoder.json.decodeFromJsonElement<CountdownComponent>(json)
"web_view" -> jsonDecoder.json.decodeFromJsonElement<WebViewComponent>(json)
"fallback_header" -> FallbackHeaderComponent
else -> json["fallback"]
?.let { it as? JsonObject }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.revenuecat.purchases.paywalls.components

import androidx.compose.runtime.Immutable
import com.revenuecat.purchases.InternalRevenueCatAPI
import com.revenuecat.purchases.paywalls.components.properties.Size
import com.revenuecat.purchases.paywalls.components.properties.SizeConstraint
import com.revenuecat.purchases.paywalls.components.properties.SizeConstraint.Fill
import dev.drewhamilton.poko.Poko
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

/**
* A Paywalls V2 component that renders generic hosted web content (a web bundle entrypoint) inside a
* web view.
*
* Only [protocolVersion] `1` is currently supported. When [protocolVersion] is present the SDK
* isolates the web content from external sources (see the content blocking applied in the UI module):
* the bundle must be fully self-contained.
*/
@InternalRevenueCatAPI
@Poko
@Serializable
@SerialName("web_view")
@Immutable
public class WebViewComponent(
@get:JvmSynthetic
public val url: String,
@get:JvmSynthetic
public val id: String? = null,
@get:JvmSynthetic
public val name: String? = null,
@get:JvmSynthetic
public val visible: Boolean? = null,
@SerialName("protocol_version")
@get:JvmSynthetic
public val protocolVersion: Int? = null,
@get:JvmSynthetic
public val size: Size = Size(width = Fill, height = SizeConstraint.Fit),
) : PaywallComponent
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import com.revenuecat.purchases.paywalls.components.TabsComponent
import com.revenuecat.purchases.paywalls.components.TextComponent
import com.revenuecat.purchases.paywalls.components.TimelineComponent
import com.revenuecat.purchases.paywalls.components.VideoComponent
import com.revenuecat.purchases.paywalls.components.WebViewComponent

/**
* Returns all PaywallComponent that satisfy the predicate.
Expand Down Expand Up @@ -74,6 +75,7 @@ internal fun PaywallComponent.filter(predicate: (PaywallComponent) -> Boolean):
is ImageComponent,
is IconComponent,
is TextComponent,
is WebViewComponent,
-> {
// These don't have child components.
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import com.revenuecat.purchases.paywalls.components.TabsComponent
import com.revenuecat.purchases.paywalls.components.TextComponent
import com.revenuecat.purchases.paywalls.components.TimelineComponent
import com.revenuecat.purchases.paywalls.components.VideoComponent
import com.revenuecat.purchases.paywalls.components.WebViewComponent
import com.revenuecat.purchases.paywalls.components.common.Background
import com.revenuecat.purchases.paywalls.components.common.ComponentOverride
import com.revenuecat.purchases.paywalls.components.common.PaywallComponentsConfig
Expand Down Expand Up @@ -115,6 +116,7 @@ internal class PaywallComponentsImagePreDownloader(
is TabControlToggleComponent,
is TextComponent,
is TimelineComponent,
is WebViewComponent,
-> emptySet()
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package com.revenuecat.purchases.paywalls.components

import com.revenuecat.purchases.JsonTools
import com.revenuecat.purchases.paywalls.components.properties.Size
import com.revenuecat.purchases.paywalls.components.properties.SizeConstraint
import com.revenuecat.purchases.utils.filter
import org.assertj.core.api.Assertions.assertThat
import org.intellij.lang.annotations.Language
import org.junit.Test
import kotlin.test.assertEquals

class WebViewComponentTests {

@Language("json")
private val fullJson = """
{
"id": "promo_web_view",
"type": "web_view",
"protocol_version": 1,
"url": "https://assets.pawwalls.com/web_bundles/123/index.html",
"size": {
"width": { "type": "fill" },
"height": { "type": "fit" }
},
"visible": true,
"name": "Promo web component"
}
"""

@Test
fun `deserializes full Khepri schema`() {
val actual = JsonTools.json.decodeFromString<PaywallComponent>(fullJson)

assertThat(actual).isInstanceOf(WebViewComponent::class.java)
val webView = actual as WebViewComponent

assertThat(webView.id).isEqualTo("promo_web_view")
assertThat(webView.name).isEqualTo("Promo web component")
assertThat(webView.visible).isTrue()
assertThat(webView.protocolVersion).isEqualTo(1)
assertThat(webView.url).isEqualTo("https://assets.pawwalls.com/web_bundles/123/index.html")
assertThat(webView.size).isEqualTo(Size(width = SizeConstraint.Fill, height = SizeConstraint.Fit))
}

@Test
fun `ignores capabilities declared by the schema`() {
// v1 isolates entirely from external sources via a fixed CSP, so any schema-declared
// capabilities are decoded-and-ignored rather than failing to parse.
@Language("json")
val json = """
{
"type": "web_view",
"url": "https://paywalls.revenuecat.com/index.html",
"capabilities": {
"network_access": { "allowed_domains": ["api.segment.io"] },
"camera": true,
"microphone": true,
"clipboard_write": true,
"clipboard_read": true,
"geolocation": true
}
Comment on lines +54 to +61

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

These are v2 capabilities which we deliberately omitted from the initial release.

}
"""

val actual = JsonTools.json.decodeFromString<PaywallComponent>(json)

assertEquals(
WebViewComponent(url = "https://paywalls.revenuecat.com/index.html"),
actual,
)
}

@Test
fun `deserializes template url correctly`() {
@Language("json")
val templateJson = """
{
"type": "web_view",
"protocol_version": 1,
"url": "https://paywalls.revenuecat.com/{{ custom.animal }}.html"
}
"""

val actual = JsonTools.json.decodeFromString<PaywallComponent>(templateJson)

assertEquals(
WebViewComponent(
url = "https://paywalls.revenuecat.com/{{ custom.animal }}.html",
protocolVersion = 1,
),
actual,
)
}

@Test
fun `deserializes minimal shape with defaults for missing optional fields`() {
// Older/partial configs may omit protocol_version and size. These should still
// decode without crashing, using defaults.
@Language("json")
val minimalJson = """
{
"type": "web_view",
"url": "https://paywalls.revenuecat.com/index.html"
}
"""

val actual = JsonTools.json.decodeFromString<PaywallComponent>(minimalJson)

assertEquals(
WebViewComponent(url = "https://paywalls.revenuecat.com/index.html"),
actual,
)
val webView = actual as WebViewComponent
assertThat(webView.protocolVersion).isNull()
assertThat(webView.size).isEqualTo(Size(width = SizeConstraint.Fill, height = SizeConstraint.Fit))
}

@Test
fun `filter treats web_view as a leaf component`() {
// web_view has no child components, so filtering a tree should return only the component itself.
val webView = WebViewComponent(url = "https://paywalls.revenuecat.com/index.html")

val matches = webView.filter { it is WebViewComponent }

assertThat(matches).containsExactly(webView)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import com.revenuecat.purchases.paywalls.components.TabsComponent
import com.revenuecat.purchases.paywalls.components.TextComponent
import com.revenuecat.purchases.paywalls.components.TimelineComponent
import com.revenuecat.purchases.paywalls.components.VideoComponent
import com.revenuecat.purchases.paywalls.components.WebViewComponent
import com.revenuecat.purchases.paywalls.components.common.Background
import com.revenuecat.purchases.paywalls.components.common.LocaleId
import com.revenuecat.purchases.paywalls.components.common.LocalizationKey
Expand Down Expand Up @@ -573,6 +574,7 @@ internal class StyleFactory(
is TabsComponent -> createTabsComponentStyle(component)
is VideoComponent -> createVideoComponentStyle(component)
is FallbackHeaderComponent -> Result.Success(null)
is WebViewComponent -> Result.Success(null)
is CountdownComponent -> createCountdownComponentStyle(
component,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import com.revenuecat.purchases.paywalls.components.TabsComponent
import com.revenuecat.purchases.paywalls.components.TextComponent
import com.revenuecat.purchases.paywalls.components.TimelineComponent
import com.revenuecat.purchases.paywalls.components.VideoComponent
import com.revenuecat.purchases.paywalls.components.WebViewComponent
import com.revenuecat.purchases.paywalls.components.common.ComponentOverride
import com.revenuecat.purchases.paywalls.components.common.LocalizationData
import com.revenuecat.purchases.paywalls.components.common.LocalizationKey
Expand Down Expand Up @@ -516,6 +517,7 @@ internal fun PaywallComponent.containsUnsupportedCondition(): Boolean = when (th
is TabControlToggleComponent -> false
is TabControlComponent -> false
is FallbackHeaderComponent -> false
is WebViewComponent -> false
}

@JvmSynthetic
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import com.revenuecat.purchases.paywalls.components.TabsComponent
import com.revenuecat.purchases.paywalls.components.TextComponent
import com.revenuecat.purchases.paywalls.components.TimelineComponent
import com.revenuecat.purchases.paywalls.components.VideoComponent
import com.revenuecat.purchases.paywalls.components.WebViewComponent
import com.revenuecat.purchases.paywalls.components.common.Background
import com.revenuecat.purchases.paywalls.components.common.ComponentOverride
import com.revenuecat.purchases.paywalls.components.common.ComponentsConfig
Expand Down Expand Up @@ -1298,7 +1299,8 @@ class TabsComponentViewTests {
is IconComponent,
is TextComponent,
is VideoComponent,
-> {
is WebViewComponent,
-> {
// These don't have child components.
}
}
Expand Down
2 changes: 1 addition & 1 deletion upstream/paywall-preview-resources