Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
5cf1dfd
feat(paywalls): public API surface for web_view messages (1/7)
cursoragent Jul 15, 2026
f7f5d16
feat(paywalls): web_view wire envelope + JSON hardening (2/7)
cursoragent Jul 15, 2026
e671378
refactor(paywalls): keep web view bridge internal
cursoragent Jul 16, 2026
41401ac
refactor(paywalls): keep web view bridge internal
cursoragent Jul 16, 2026
d941c01
Merge updated internal web view API base
cursoragent Jul 16, 2026
ca80c8f
Merge branch 'main' into webview/1-public-api
ajpallares Jul 17, 2026
94982da
fix(paywalls): remove client-side web view payload limits
cursoragent Jul 17, 2026
ea6d24b
test(paywalls): address web view envelope review
cursoragent Jul 17, 2026
0612595
refactor(paywalls): remove unused web view handler API
cursoragent Jul 17, 2026
d14d63c
fix(paywalls): retain internal bridge dependencies
cursoragent Jul 17, 2026
19cbfe6
refactor(paywalls): remove web view message handler concept
JZDesign Jul 17, 2026
c3d27a9
Merge branch 'webview/1-public-api' into webview/2-envelope
JZDesign Jul 17, 2026
d829317
Merge branch 'main' into webview/1-public-api
ajpallares Jul 20, 2026
643c32a
Merge branch 'webview/1-public-api' into webview/2-envelope
ajpallares Jul 20, 2026
1a82768
Merge branch 'main-latest' into webview/2-envelope
ajpallares Jul 20, 2026
328ebf9
Rename PaywallWebViewAPI.swift to PaywallWebViewValue.swift, add head…
ajpallares Jul 20, 2026
4243461
Add envelope wire-key/rich-payload/rejection tests and brief Envelope…
ajpallares Jul 20, 2026
306e38d
Drop reservedPayloadKeys from envelope and simplify test helper
ajpallares Jul 20, 2026
3ca5794
Merge branch 'main' into webview/2-envelope
ajpallares Jul 20, 2026
b701470
Trim unused message types and move receiveScript out of the data model
ajpallares Jul 20, 2026
d5ceb16
Merge branch 'main' into webview/2-envelope
ajpallares Jul 21, 2026
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
14 changes: 10 additions & 4 deletions RevenueCat.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@
16DA8ECD2E4EE33700283940 /* ImageComponentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16DA8ECC2E4EE33700283940 /* ImageComponentTests.swift */; };
16DA8EDF2E4F6A6600283940 /* PaywallVideoComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16DA8EDE2E4F6A6600283940 /* PaywallVideoComponent.swift */; };
16DA8EF42E4F7A2500283940 /* VideoComponentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16DA8EF32E4F7A2500283940 /* VideoComponentTests.swift */; };
FA4A2DF41FC434A9401F08CC /* PaywallWebViewAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D0C57ECDF0E7F1A5B4418FF /* PaywallWebViewAPI.swift */; };
FA4A2DF41FC434A9401F08CC /* PaywallWebViewValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D0C57ECDF0E7F1A5B4418FF /* PaywallWebViewValue.swift */; };
11CC2EA83C56933E40D8E5D8 /* WebViewEnvelope.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89DD761C9E00E61032AAEB44 /* WebViewEnvelope.swift */; };
16DA8F1E2E4FB6E200283940 /* ImageComponent.json in Resources */ = {isa = PBXBuildFile; fileRef = 16DA8F1B2E4FB6E200283940 /* ImageComponent.json */; };
16DA8F1F2E4FB6E200283940 /* VideoComponent.json in Resources */ = {isa = PBXBuildFile; fileRef = 16DA8F1C2E4FB6E200283940 /* VideoComponent.json */; };
16E146AD2E99F3480089B609 /* TransactionNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16E146AB2E99F1E20089B609 /* TransactionNotifications.swift */; };
Expand Down Expand Up @@ -1672,7 +1673,9 @@
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>"; };
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>"; };
9D0C57ECDF0E7F1A5B4418FF /* PaywallWebViewValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallWebViewValue.swift; sourceTree = "<group>"; };
89DD761C9E00E61032AAEB44 /* WebViewEnvelope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewEnvelope.swift; sourceTree = "<group>"; };
C1B939C7DAAFEA1DE2DE2674 /* WebViewEnvelopeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewEnvelopeTests.swift; sourceTree = "<group>"; };
686562C08D953A27986DB276 /* PaywallWebViewValueTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallWebViewValueTests.swift; sourceTree = "<group>"; };
16DA8F1B2E4FB6E200283940 /* ImageComponent.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = ImageComponent.json; sourceTree = "<group>"; };
16DA8F1C2E4FB6E200283940 /* VideoComponent.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = VideoComponent.json; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3237,6 +3240,7 @@
isa = PBXGroup;
children = (
686562C08D953A27986DB276 /* PaywallWebViewValueTests.swift */,
C1B939C7DAAFEA1DE2DE2674 /* WebViewEnvelopeTests.swift */,
802593842FE07057005AF6DF /* PaywallStateStoreTests.swift */,
16A9F7B62FAA22D0008E8A4D /* VideoComponentViewTests.swift */,
A9D45E4EAFAD4C0385BEDFB3 /* VideoPlayerViewTests.swift */,
Expand Down Expand Up @@ -3374,7 +3378,8 @@
EB23E44596DD9714E81840CF /* WebView */ = {
isa = PBXGroup;
children = (
9D0C57ECDF0E7F1A5B4418FF /* PaywallWebViewAPI.swift */,
9D0C57ECDF0E7F1A5B4418FF /* PaywallWebViewValue.swift */,
89DD761C9E00E61032AAEB44 /* WebViewEnvelope.swift */,
);
path = WebView;
sourceTree = "<group>";
Expand Down Expand Up @@ -8326,7 +8331,8 @@
164681D02E6B577600854AA5 /* VideoComponentView.swift in Sources */,
164681D12E6B577600854AA5 /* VideoPlayerViewUIView.swift in Sources */,
164681D62E6B577600854AA5 /* VideoPlayerLayerUIView.swift in Sources */,
FA4A2DF41FC434A9401F08CC /* PaywallWebViewAPI.swift in Sources */,
FA4A2DF41FC434A9401F08CC /* PaywallWebViewValue.swift in Sources */,
11CC2EA83C56933E40D8E5D8 /* WebViewEnvelope.swift in Sources */,
DBAA1FC22F8D4ED8000E8C81 /* HeaderNestedHeroZLayerSafeAreaPreview.swift in Sources */,
164681D22E6B577600854AA5 /* VideoComponentViewModel.swift in Sources */,
2C7457422CE81107004ACE52 /* IntroOfferEligibilityContext.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
//
// Copyright RevenueCat Inc. All Rights Reserved.
//

import Foundation

#if !os(tvOS) // For Paywalls V2
Expand Down
113 changes: 113 additions & 0 deletions RevenueCatUI/Templates/V2/Components/WebView/WebViewEnvelope.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import Foundation

#if !os(tvOS) // For Paywalls V2

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
enum WebViewEnvelope {

static let channel = "rc-web-components"
static let messageHandlerName = "rcWebComponents"
static let receiveFunction = "__rcWebComponentsReceive"
static let defaultProtocolVersion = 1

static let messageTypeStepLoaded = "rc:step-loaded"
static let messageTypeStepComplete = "rc:step-complete"
static let messageTypeRequestVariables = "rc:request-variables"
static let messageTypeVariables = "rc:variables"
static let messageTypeError = "rc:error"
Comment thread
ajpallares marked this conversation as resolved.
Outdated
static let messageTypeResize = "resize"
static let messageTypeFit = "fit"

static let maxResizePoints: CGFloat = 10_000
static let resizeThreshold: CGFloat = 1
static let fallbackFitHeight: CGFloat = 100
static let fallbackFitWidth: CGFloat = 300

// swiftlint:disable nesting
enum Kind: String, Codable {
case connect
case `init`
case reject
case message
case request
case response
case error
}

/// One JSON message exchanged with the `web_view` content. Fields used vary by ``kind``.
struct Envelope: Codable, Equatable {
let channel: String
let protocolVersion: Int
let kind: Kind
let componentID: String
let type: String?
let id: String?
let payload: [String: PaywallWebViewValue]?
let error: String?

init(
kind: Kind,
componentID: String,
type: String? = nil,
id: String? = nil,
payload: [String: PaywallWebViewValue]? = nil,
error: String? = nil,
protocolVersion: Int = WebViewEnvelope.defaultProtocolVersion
) {
self.channel = WebViewEnvelope.channel
self.protocolVersion = protocolVersion
self.kind = kind
self.componentID = componentID
self.type = type
self.id = id
self.payload = payload
self.error = error
}

enum CodingKeys: String, CodingKey {
case channel
case protocolVersion = "protocol_version"
case kind
case componentID = "component_id"
case type
case id
case payload
case error
}
}
// swiftlint:enable nesting

static func decode(rawMessage: Any) -> Envelope? {
// `workflow-web-components-sdk` sends frames with `postMessage(JSON.stringify(frame))`.
guard let string = rawMessage as? String,
let data = string.data(using: .utf8) else {
return nil
}
Comment thread
cursor[bot] marked this conversation as resolved.

guard let envelope = try? JSONDecoder().decode(Envelope.self, from: data),
envelope.channel == Self.channel else {
return nil
}

return envelope
}

static func receiveScript(for envelope: Envelope) -> String? {
Comment thread
ajpallares marked this conversation as resolved.
Outdated
let encoder = JSONEncoder()
guard let data = try? encoder.encode(envelope),
let json = String(data: data, encoding: .utf8) else {
return nil
}
let escaped = json
.replacingOccurrences(of: "\u{2028}", with: "\\u2028")
.replacingOccurrences(of: "\u{2029}", with: "\\u2029")

return """
(function(){var m=\(escaped);if(typeof window.\(Self.receiveFunction)==='function'){\
window.\(Self.receiveFunction)(m);}})();
"""
}

}

#endif
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,36 @@ final class PaywallWebViewValueTests: TestCase {
XCTAssertEqual(decoded.objectValue?["number"]?.numberValue, 1.25)
XCTAssertEqual(decoded.objectValue?["bool"]?.boolValue, true)
XCTAssertTrue(decoded.objectValue?["null"]?.isNull == true)

let array = try XCTUnwrap(decoded.objectValue?["array"]?.arrayValue)
XCTAssertEqual(array.count, 2)
XCTAssertTrue(array.first?.isNull == true)
XCTAssertEqual(array.last?.stringValue, "x")

XCTAssertEqual(decoded.objectValue?["object"]?.objectValue?["nested"]?.boolValue, false)
}

func testTopLevelScalarsAndEmptyContainersRoundTrip() throws {
let values: [PaywallWebViewValue] = [
.string("hello"),
.number(1.25),
.bool(true),
.bool(false),
.null,
.array([]),
.object([:])
]

for value in values {
let decoded = try JSONDecoder().decode(
PaywallWebViewValue.self,
from: try JSONEncoder().encode(value)
)
XCTAssertEqual(decoded, value)
}

XCTAssertEqual(PaywallWebViewValue.array([]).arrayValue, [])
XCTAssertEqual(PaywallWebViewValue.object([:]).objectValue, [:])
}

func testNumberBoolDisambiguation() throws {
Expand Down
Loading
Loading