Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
8 changes: 8 additions & 0 deletions RevenueCat.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,9 @@
16DA8EC92E4EE24100283940 /* JsonLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16DA8EC82E4EE24100283940 /* JsonLoader.swift */; };
16DA8ECD2E4EE33700283940 /* ImageComponentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16DA8ECC2E4EE33700283940 /* ImageComponentTests.swift */; };
16DA8EDF2E4F6A6600283940 /* PaywallVideoComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16DA8EDE2E4F6A6600283940 /* PaywallVideoComponent.swift */; };
66EAEED2B259D54A9E789F4A /* PaywallWebViewComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C1ACED1916C08B0E8E2776 /* PaywallWebViewComponent.swift */; };
16DA8EF42E4F7A2500283940 /* VideoComponentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16DA8EF32E4F7A2500283940 /* VideoComponentTests.swift */; };
E7EB64E9D44783DD16A28B06 /* WebViewComponentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7EB64E9D44783DD16A28B05 /* WebViewComponentTests.swift */; };
FA4A2DF41FC434A9401F08CC /* PaywallWebViewAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D0C57ECDF0E7F1A5B4418FF /* PaywallWebViewAPI.swift */; };
16DA8F1E2E4FB6E200283940 /* ImageComponent.json in Resources */ = {isa = PBXBuildFile; fileRef = 16DA8F1B2E4FB6E200283940 /* ImageComponent.json */; };
16DA8F1F2E4FB6E200283940 /* VideoComponent.json in Resources */ = {isa = PBXBuildFile; fileRef = 16DA8F1C2E4FB6E200283940 /* VideoComponent.json */; };
Expand Down Expand Up @@ -1671,6 +1673,8 @@
16DA8EC82E4EE24100283940 /* JsonLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JsonLoader.swift; sourceTree = "<group>"; };
16DA8ECC2E4EE33700283940 /* ImageComponentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageComponentTests.swift; sourceTree = "<group>"; };
16DA8EDE2E4F6A6600283940 /* PaywallVideoComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallVideoComponent.swift; sourceTree = "<group>"; };
14C1ACED1916C08B0E8E2776 /* PaywallWebViewComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallWebViewComponent.swift; sourceTree = "<group>"; };
E7EB64E9D44783DD16A28B05 /* WebViewComponentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewComponentTests.swift; sourceTree = "<group>"; };
16DA8EF32E4F7A2500283940 /* VideoComponentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoComponentTests.swift; sourceTree = "<group>"; };
9D0C57ECDF0E7F1A5B4418FF /* PaywallWebViewAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallWebViewAPI.swift; sourceTree = "<group>"; };
686562C08D953A27986DB276 /* PaywallWebViewValueTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallWebViewValueTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3236,6 +3240,7 @@
030890822D2B77DD0069677B /* PaywallsV2 */ = {
isa = PBXGroup;
children = (
E7EB64E9D44783DD16A28B05 /* WebViewComponentTests.swift */,
686562C08D953A27986DB276 /* PaywallWebViewValueTests.swift */,
802593842FE07057005AF6DF /* PaywallStateStoreTests.swift */,
16A9F7B62FAA22D0008E8A4D /* VideoComponentViewTests.swift */,
Expand Down Expand Up @@ -5965,6 +5970,7 @@
isa = PBXGroup;
children = (
16DA8EDE2E4F6A6600283940 /* PaywallVideoComponent.swift */,
14C1ACED1916C08B0E8E2776 /* PaywallWebViewComponent.swift */,
16A7F4C02E563B89001F9FC8 /* Transitions */,
03C730F22D35F13F00297FEC /* PaywallV2CacheWarming.swift */,
2CC791612CC0493600FBE120 /* Common */,
Expand Down Expand Up @@ -7527,6 +7533,7 @@
4F8929192A65EF3000A91EA2 /* EnsureNonEmptyCollectionDecodable.swift in Sources */,
757E73CE2F0FE2410066DCDC /* LocalTransactionMetadata.swift in Sources */,
16DA8EDF2E4F6A6600283940 /* PaywallVideoComponent.swift in Sources */,
66EAEED2B259D54A9E789F4A /* PaywallWebViewComponent.swift in Sources */,
35F82BB626A9B8040051DF03 /* AttributionDataMigrator.swift in Sources */,
4F5F47082AA91F8A005649D8 /* HealthOperation.swift in Sources */,
4FEF41AB2B4F2F3400CD699F /* MapAppStoreDetector.swift in Sources */,
Expand Down Expand Up @@ -7890,6 +7897,7 @@
351B51BA26D450E800BD2BD7 /* ProductRequestDataExtensions.swift in Sources */,
57E6C27E29723F9E001AFE98 /* SigningTests.swift in Sources */,
16DA8EF42E4F7A2500283940 /* VideoComponentTests.swift in Sources */,
E7EB64E9D44783DD16A28B06 /* WebViewComponentTests.swift in Sources */,
574A2F3F282D75E300150D40 /* OfferingsDecodingTests.swift in Sources */,
3AC9CEA8FC6D49A3EB7D7393 /* PublishedWorkflowCodableTests.swift in Sources */,
35E840CE2710E2EB00899AE2 /* MockManageSubscriptionsHelper.swift in Sources */,
Expand Down
96 changes: 96 additions & 0 deletions Sources/Paywalls/Components/PaywallWebViewComponent.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
//
// Copyright RevenueCat Inc. All Rights Reserved.
//
// Licensed under the MIT License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://opensource.org/licenses/MIT
//
// PaywallWebViewComponent.swift
//
// swiftlint:disable missing_docs nesting

import Foundation

@_spi(Internal) public extension PaywallComponent {

final class WebViewComponent: PaywallComponentBase {

/// Wire `type` value. `ComponentType.webView` is registered in the activation PR.
let type: String
public let id: String?
public let name: String?
public let visible: Bool?

/// The declared Paywalls web component protocol version.
/// Decoded and preserved; native host capability is fixed by the SDK build.
public let protocolVersion: Int

/// The static HTTPS URL of the web bundle entry point.
public let url: String

public let size: Size

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Since size is a required field always sent by the backend (see https://github.com/RevenueCat/khepri/blob/main/khepri/services/paywalls/domain/template_schemas/v2/components/web_view.py#L33), I've removed in 5a57be9 the custom init(from decoder) that was only used to assign a default value to the size property


public init(
id: String? = nil,
name: String? = nil,
visible: Bool? = nil,
protocolVersion: Int = 1,
url: String,
size: Size = .init(width: .fill, height: .fit(nil))
) {
self.type = "web_view"
self.id = id
self.name = name
self.visible = visible
self.protocolVersion = protocolVersion
self.url = url
self.size = size
}

private enum CodingKeys: String, CodingKey {
case type
case id
case name
case visible
case protocolVersion
case url
case size
}

required public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.type = try container.decode(String.self, forKey: .type)
self.id = try container.decodeIfPresent(String.self, forKey: .id)
self.name = try container.decodeIfPresent(String.self, forKey: .name)
self.visible = try container.decodeIfPresent(Bool.self, forKey: .visible)
self.protocolVersion = try container.decodeIfPresent(Int.self, forKey: .protocolVersion) ?? 1
self.url = try container.decode(String.self, forKey: .url)
self.size = try container.decodeIfPresent(Size.self, forKey: .size)
?? .init(width: .fill, height: .fit(nil))
}
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated

public func hash(into hasher: inout Hasher) {
hasher.combine(type)
hasher.combine(id)
hasher.combine(name)
hasher.combine(visible)
hasher.combine(protocolVersion)
hasher.combine(url)
hasher.combine(size)
}

public static func == (lhs: WebViewComponent, rhs: WebViewComponent) -> Bool {
return lhs.type == rhs.type &&
lhs.id == rhs.id &&
lhs.name == rhs.name &&
lhs.visible == rhs.visible &&
lhs.protocolVersion == rhs.protocolVersion &&
lhs.url == rhs.url &&
lhs.size == rhs.size
}

}

}
108 changes: 108 additions & 0 deletions Tests/RevenueCatUITests/PaywallsV2/WebViewComponentTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
//
// Copyright RevenueCat Inc. All Rights Reserved.
//

@_spi(Internal) @testable import RevenueCat
import XCTest

#if !os(tvOS)

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
final class WebViewComponentTests: TestCase {

func testDecodesMinimalJSONDefaultsAndIgnoresUnknownKeys() throws {
let minimal = try JSONDecoder.default.decode(PaywallComponent.WebViewComponent.self, from: Data("""
{ "type": "web_view", "url": "https://example.com", "unknown": true }
""".utf8))

XCTAssertNil(minimal.id)
XCTAssertNil(minimal.visible)
XCTAssertEqual(minimal.protocolVersion, 1)
XCTAssertEqual(minimal.size.width, .fill)
XCTAssertEqual(minimal.size.height, .fit(nil))
XCTAssertEqual(minimal.url, "https://example.com")
XCTAssertEqual(minimal.type, "web_view")
}

func testDecodesExplicitFields() throws {
let component = try JSONDecoder.default.decode(PaywallComponent.WebViewComponent.self, from: Data("""
{
"type": "web_view",
"id": "web",
"name": "Survey",
"visible": false,
"protocol_version": 2,
"url": "https://example.com/index.html",
"size": { "width": { "type": "fixed", "value": 320 }, "height": { "type": "fit" } }
}
""".utf8))

XCTAssertEqual(component.id, "web")
XCTAssertEqual(component.name, "Survey")
XCTAssertEqual(component.visible, false)
XCTAssertEqual(component.protocolVersion, 2)
XCTAssertEqual(component.url, "https://example.com/index.html")
}

func testEncodeDecodeRoundTripUsesSnakeCaseWireKeys() throws {
let component = PaywallComponent.WebViewComponent(
id: "web",
name: "Survey",
visible: true,
protocolVersion: 2,
url: "https://example.com/index.html"
)

let data = try JSONEncoder.default.encode(component)
let json = try XCTUnwrap(String(data: data, encoding: .utf8))
XCTAssertTrue(json.contains("\"protocol_version\""))
XCTAssertTrue(json.contains("\"web_view\""))

let decoded = try JSONDecoder.default.decode(PaywallComponent.WebViewComponent.self, from: data)
XCTAssertEqual(decoded, component)
}

func testDecodingWithoutURLThrows() {
XCTAssertThrowsError(
try JSONDecoder.default.decode(PaywallComponent.WebViewComponent.self, from: Data("""
{ "type": "web_view" }
""".utf8))
)
}

func testPaywallComponentTreatsWebViewAsUnknownAndUsesFallback() throws {
let decoded = try JSONDecoder.default.decode(PaywallComponent.self, from: Data("""
{
"type": "web_view",
"url": "https://example.com",
"fallback": \(Self.fallbackStackJSON)
}
""".utf8))

guard case .stack = decoded else {
return XCTFail("web_view should be unknown to PaywallComponent and fall back to the stack")
}
}

func testPaywallComponentWebViewWithoutFallbackThrows() {
XCTAssertThrowsError(
try JSONDecoder.default.decode(PaywallComponent.self, from: Data("""
{ "type": "web_view", "url": "https://example.com" }
""".utf8))
)
}

private static let fallbackStackJSON = """
{
"type": "stack",
"dimension": { "type": "vertical", "alignment": "center", "distribution": "start" },
"size": { "width": { "type": "fill" }, "height": { "type": "fill" } },
"padding": { "top": 0, "bottom": 0, "leading": 0, "trailing": 0 },
"margin": { "top": 0, "bottom": 0, "leading": 0, "trailing": 0 },
"components": []
}
"""

}

#endif