From 5cf1dfd3a5001b0811b8119907e8093ae5ca5a77 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 15 Jul 2026 20:18:13 +0000 Subject: [PATCH 01/13] feat(paywalls): public API surface for web_view messages (1/7) Add PaywallWebViewMessage/Value/Controller/Action types and API baselines. Controller post APIs are no-ops until the session PR wires WebKit. Co-authored-by: Jacob Rakidzich --- RevenueCat.xcodeproj/project.pbxproj | 14 ++ .../WebView/PaywallWebViewAPI.swift | 205 ++++++++++++++++++ .../AllAPITests.xcodeproj/project.pbxproj | 4 + .../PaywallWebViewAPI.swift | 77 +++++++ .../PaywallsV2/PaywallWebViewValueTests.swift | 59 +++++ ...enuecatui-api-ios-simulator.swiftinterface | 62 ++++++ api/revenuecatui-api-ios.swiftinterface | 62 ++++++ api/revenuecatui-api-macos.swiftinterface | 62 ++++++ ...atui-api-visionos-simulator.swiftinterface | 62 ++++++ api/revenuecatui-api-visionos.swiftinterface | 62 ++++++ ...catui-api-watchos-simulator.swiftinterface | 62 ++++++ api/revenuecatui-api-watchos.swiftinterface | 62 ++++++ 12 files changed, 793 insertions(+) create mode 100644 RevenueCatUI/Templates/V2/Components/WebView/PaywallWebViewAPI.swift create mode 100644 Tests/APITesters/AllAPITests/RevenueCatUISwiftAPITester/PaywallWebViewAPI.swift create mode 100644 Tests/RevenueCatUITests/PaywallsV2/PaywallWebViewValueTests.swift diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 996c1ac30c..0f4bd8f8c2 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -107,6 +107,7 @@ 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 */; }; 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 */; }; @@ -1636,6 +1637,8 @@ 16DA8ECC2E4EE33700283940 /* ImageComponentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageComponentTests.swift; sourceTree = ""; }; 16DA8EDE2E4F6A6600283940 /* PaywallVideoComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallVideoComponent.swift; sourceTree = ""; }; 16DA8EF32E4F7A2500283940 /* VideoComponentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoComponentTests.swift; sourceTree = ""; }; + 9D0C57ECDF0E7F1A5B4418FF /* PaywallWebViewAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallWebViewAPI.swift; sourceTree = ""; }; + 686562C08D953A27986DB276 /* PaywallWebViewValueTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallWebViewValueTests.swift; sourceTree = ""; }; 16DA8F1B2E4FB6E200283940 /* ImageComponent.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = ImageComponent.json; sourceTree = ""; }; 16DA8F1C2E4FB6E200283940 /* VideoComponent.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = VideoComponent.json; sourceTree = ""; }; 16E146AB2E99F1E20089B609 /* TransactionNotifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionNotifications.swift; sourceTree = ""; }; @@ -3165,6 +3168,7 @@ 030890822D2B77DD0069677B /* PaywallsV2 */ = { isa = PBXGroup; children = ( + 686562C08D953A27986DB276 /* PaywallWebViewValueTests.swift */, 802593842FE07057005AF6DF /* PaywallStateStoreTests.swift */, 16A9F7B62FAA22D0008E8A4D /* VideoComponentViewTests.swift */, A9D45E4EAFAD4C0385BEDFB3 /* VideoPlayerViewTests.swift */, @@ -3297,6 +3301,14 @@ path = Video; sourceTree = ""; }; + EB23E44596DD9714E81840CF /* WebView */ = { + isa = PBXGroup; + children = ( + 9D0C57ECDF0E7F1A5B4418FF /* PaywallWebViewAPI.swift */, + ); + path = WebView; + sourceTree = ""; + }; 1699BD5C2EEFBEEC002001FB /* DefaultPaywall */ = { isa = PBXGroup; children = ( @@ -3387,6 +3399,7 @@ 0387D4AD2EC58AA5008E4A6B /* Countdown */, E1C0A00BBEEF0001CCDD0001 /* Header */, 164681CD2E6B577600854AA5 /* Video */, + EB23E44596DD9714E81840CF /* WebView */, 2C7457472CEA66AB004ACE52 /* ComponentsView.swift */, 7707A94A2CAD936A006E0313 /* Button */, 03C06FC82D479C6300600693 /* Carousel */, @@ -8233,6 +8246,7 @@ 164681D02E6B577600854AA5 /* VideoComponentView.swift in Sources */, 164681D12E6B577600854AA5 /* VideoPlayerViewUIView.swift in Sources */, 164681D62E6B577600854AA5 /* VideoPlayerLayerUIView.swift in Sources */, + FA4A2DF41FC434A9401F08CC /* PaywallWebViewAPI.swift in Sources */, DBAA1FC22F8D4ED8000E8C81 /* HeaderNestedHeroZLayerSafeAreaPreview.swift in Sources */, 164681D22E6B577600854AA5 /* VideoComponentViewModel.swift in Sources */, 2C7457422CE81107004ACE52 /* IntroOfferEligibilityContext.swift in Sources */, diff --git a/RevenueCatUI/Templates/V2/Components/WebView/PaywallWebViewAPI.swift b/RevenueCatUI/Templates/V2/Components/WebView/PaywallWebViewAPI.swift new file mode 100644 index 0000000000..5ba2a3fe3b --- /dev/null +++ b/RevenueCatUI/Templates/V2/Components/WebView/PaywallWebViewAPI.swift @@ -0,0 +1,205 @@ +import Foundation +import SwiftUI +// swiftlint:disable missing_docs + +#if !os(tvOS) // For Paywalls V2 + +/// A validated message sent from a Paywalls V2 `web_view` component to your app. +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +public struct PaywallWebViewMessage: Sendable, Equatable { + + /// The identifier of the `web_view` component that produced this message. + public let componentID: String + + /// The message type, e.g. `"rc:step-complete"`. + public let type: String + + /// The responses collected by the web flow. Only populated for `"rc:step-complete"` messages. + public let responses: [String: PaywallWebViewValue]? + + /// The error reported by the web content. Only populated for `"rc:error"` messages. + public let error: String? + + public init( + componentID: String, + type: String, + responses: [String: PaywallWebViewValue]? = nil, + error: String? = nil + ) { + self.componentID = componentID + self.type = type + self.responses = responses + self.error = error + } + +} + +/// A JSON-compatible value exchanged between a Paywalls V2 `web_view` component and your app. +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +public struct PaywallWebViewValue: Sendable, Equatable, Hashable, Codable { + + private indirect enum Storage: Sendable, Equatable, Hashable { + case string(String) + case number(Double) + case bool(Bool) + case array([PaywallWebViewValue]) + case object([String: PaywallWebViewValue]) + case null + } + + private let storage: Storage + + private init(_ storage: Storage) { + self.storage = storage + } + + /// Creates a string value. + public static func string(_ value: String) -> Self { Self(.string(value)) } + + /// Creates a numeric value. Non-finite values normalize to ``null`` because JSON cannot encode them. + public static func number(_ value: Double) -> Self { value.isFinite ? Self(.number(value)) : .null } + + /// Creates a boolean value. + public static func bool(_ value: Bool) -> Self { Self(.bool(value)) } + + /// Creates an array value. + public static func array(_ value: [PaywallWebViewValue]) -> Self { Self(.array(value)) } + + /// Creates an object (dictionary) value. + public static func object(_ value: [String: PaywallWebViewValue]) -> Self { Self(.object(value)) } + + /// A null value. + public static var null: Self { Self(.null) } + + public var stringValue: String? { + if case .string(let value) = self.storage { return value } + return nil + } + + public var numberValue: Double? { + if case .number(let value) = self.storage { return value } + return nil + } + + public var boolValue: Bool? { + if case .bool(let value) = self.storage { return value } + return nil + } + + public var arrayValue: [PaywallWebViewValue]? { + if case .array(let value) = self.storage { return value } + return nil + } + + public var objectValue: [String: PaywallWebViewValue]? { + if case .object(let value) = self.storage { return value } + return nil + } + + public var isNull: Bool { + if case .null = self.storage { return true } + return false + } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + if container.decodeNil() { + self = .null + } else if let value = try? container.decode(Bool.self) { + self = .bool(value) + } else if let value = try? container.decode(Double.self) { + self = .number(value) + } else if let value = try? container.decode(String.self) { + self = .string(value) + } else if let value = try? container.decode([PaywallWebViewValue].self) { + self = .array(value) + } else { + self = .object(try container.decode([String: PaywallWebViewValue].self)) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + + switch self.storage { + case .string(let value): + try container.encode(value) + case .number(let value): + try container.encode(value) + case .bool(let value): + try container.encode(value) + case .array(let value): + try container.encode(value) + case .object(let value): + try container.encode(value) + case .null: + try container.encodeNil() + } + } + +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@MainActor +public struct PaywallWebViewController { + + init() {} + + public func postVariables(componentID: String, variables: [String: PaywallWebViewValue]) { + // Session wiring lands in a later PR; public API is inert until then. + } + + public func postMessage(componentID: String, type: String, variables: [String: PaywallWebViewValue]) { + // Session wiring lands in a later PR; public API is inert until then. + } + +} + +/// A wrapper for the Paywalls V2 `web_view` message handler. +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +public struct PaywallWebViewMessageAction { + + private let action: @MainActor (PaywallWebViewMessage, PaywallWebViewController) -> Void + + public init( + _ action: @escaping @MainActor (PaywallWebViewMessage, PaywallWebViewController) -> Void + ) { + self.action = action + } + + @MainActor + public func callAsFunction(_ message: PaywallWebViewMessage, _ controller: PaywallWebViewController) { + self.action(message, controller) + } + +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +struct PaywallWebViewMessageActionKey: EnvironmentKey { + static let defaultValue: PaywallWebViewMessageAction? = nil +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +extension EnvironmentValues { + + var paywallWebViewMessageAction: PaywallWebViewMessageAction? { + get { self[PaywallWebViewMessageActionKey.self] } + set { self[PaywallWebViewMessageActionKey.self] = newValue } + } + +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +extension View { + + /// Invokes the given closure when a Paywalls V2 `web_view` component sends a message. + public func onPaywallWebViewMessage( + _ action: @escaping @MainActor (PaywallWebViewMessage, PaywallWebViewController) -> Void + ) -> some View { + self.environment(\.paywallWebViewMessageAction, PaywallWebViewMessageAction(action)) + } + +} + +#endif diff --git a/Tests/APITesters/AllAPITests/AllAPITests.xcodeproj/project.pbxproj b/Tests/APITesters/AllAPITests/AllAPITests.xcodeproj/project.pbxproj index 9cc8116989..74a25fd257 100644 --- a/Tests/APITesters/AllAPITests/AllAPITests.xcodeproj/project.pbxproj +++ b/Tests/APITesters/AllAPITests/AllAPITests.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 1E8A60812CDCF29D0034ACF3 /* RedeemWebPurchasesAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E8A60802CDCF29D0034ACF3 /* RedeemWebPurchasesAPI.swift */; }; + B096F58208E0AA6582F34133 /* PaywallWebViewAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0DC08831A62751743D823FA /* PaywallWebViewAPI.swift */; }; 1EB697C22CD26218003000FC /* WebPurchaseRedemptionResultAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EB697C12CD26218003000FC /* WebPurchaseRedemptionResultAPI.swift */; }; 2D4C60E12C5AD1BC00A29FD2 /* AllAPITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2D4C60D82C5AD1BC00A29FD2 /* AllAPITests.framework */; }; 2D4C60E62C5AD1BC00A29FD2 /* AllAPITestsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D4C60E52C5AD1BC00A29FD2 /* AllAPITestsTests.swift */; }; @@ -228,6 +229,7 @@ /* Begin PBXFileReference section */ 1E8A60802CDCF29D0034ACF3 /* RedeemWebPurchasesAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedeemWebPurchasesAPI.swift; sourceTree = ""; }; + B0DC08831A62751743D823FA /* PaywallWebViewAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallWebViewAPI.swift; sourceTree = ""; }; 1EB697C12CD26218003000FC /* WebPurchaseRedemptionResultAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebPurchaseRedemptionResultAPI.swift; sourceTree = ""; }; 2D4C60D82C5AD1BC00A29FD2 /* AllAPITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AllAPITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 2D4C60DB2C5AD1BC00A29FD2 /* AllAPITests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AllAPITests.h; sourceTree = ""; }; @@ -645,6 +647,7 @@ children = ( FD33CD722D035876000D13A4 /* CustomerCenterViewControllerAPI.swift */, 2D4C622D2C5ADA6700A29FD2 /* PaywallViewAPI.swift */, + B0DC08831A62751743D823FA /* PaywallWebViewAPI.swift */, 1E8A60802CDCF29D0034ACF3 /* RedeemWebPurchasesAPI.swift */, 2D4C622B2C5ADA6700A29FD2 /* PaywallViewControllerAPI.swift */, 2D4C622C2C5ADA6700A29FD2 /* main.swift */, @@ -1119,6 +1122,7 @@ FDCC983A2D0A0FD500F5854B /* CustomerCenterViewAPI.swift in Sources */, 2D4C62302C5ADA6700A29FD2 /* PaywallViewControllerAPI.swift in Sources */, 1E8A60812CDCF29D0034ACF3 /* RedeemWebPurchasesAPI.swift in Sources */, + B096F58208E0AA6582F34133 /* PaywallWebViewAPI.swift in Sources */, 2D4C622E2C5ADA6700A29FD2 /* PaywallViewAPI.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Tests/APITesters/AllAPITests/RevenueCatUISwiftAPITester/PaywallWebViewAPI.swift b/Tests/APITesters/AllAPITests/RevenueCatUISwiftAPITester/PaywallWebViewAPI.swift new file mode 100644 index 0000000000..a8a2986ca9 --- /dev/null +++ b/Tests/APITesters/AllAPITests/RevenueCatUISwiftAPITester/PaywallWebViewAPI.swift @@ -0,0 +1,77 @@ +// +// PaywallWebViewAPI.swift +// RevenueCatUISwiftAPITester +// +// Compile-time checks for the Paywalls V2 web_view public API surface. +// + +import RevenueCat +import RevenueCatUI +import SwiftUI + +#if !os(tvOS) // For Paywalls V2 + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +struct PaywallWebViewApp: View { + + var body: some View { + Text("") + .onPaywallWebViewMessage { (message: PaywallWebViewMessage, controller: PaywallWebViewController) in + let componentID: String = message.componentID + let type: String = message.type + let responses: [String: PaywallWebViewValue]? = message.responses + let error: String? = message.error + + controller.postVariables( + componentID: componentID, + variables: ["custom": .object(["plan": .string("annual")])] + ) + controller.postMessage(componentID: componentID, type: type, variables: [:]) + + _ = (responses, error) + } + } + +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +func checkPaywallWebViewMessageAPI() { + let message = PaywallWebViewMessage( + componentID: "", + type: "rc:step-complete", + responses: ["selected_plan": .string("annual")], + error: nil + ) + let sameMessage: Bool = message == message + _ = sameMessage +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +func checkPaywallWebViewValueAPI() { + let string: PaywallWebViewValue = .string("s") + let number: PaywallWebViewValue = .number(1.5) + let bool: PaywallWebViewValue = .bool(true) + let array: PaywallWebViewValue = .array([string, number]) + let object: PaywallWebViewValue = .object(["key": bool]) + let null: PaywallWebViewValue = .null + + let _: String? = string.stringValue + let _: Double? = number.numberValue + let _: Bool? = bool.boolValue + let _: [PaywallWebViewValue]? = array.arrayValue + let _: [String: PaywallWebViewValue]? = object.objectValue + let _: Bool = null.isNull + let _: Set = [string, number] +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@MainActor +func checkPaywallWebViewMessageActionAPI( + message: PaywallWebViewMessage, + controller: PaywallWebViewController +) { + let action = PaywallWebViewMessageAction { (_: PaywallWebViewMessage, _: PaywallWebViewController) in } + action(message, controller) +} + +#endif diff --git a/Tests/RevenueCatUITests/PaywallsV2/PaywallWebViewValueTests.swift b/Tests/RevenueCatUITests/PaywallsV2/PaywallWebViewValueTests.swift new file mode 100644 index 0000000000..a593eeb9c5 --- /dev/null +++ b/Tests/RevenueCatUITests/PaywallsV2/PaywallWebViewValueTests.swift @@ -0,0 +1,59 @@ +// +// Copyright RevenueCat Inc. All Rights Reserved. +// + +@testable import RevenueCatUI +import XCTest + +#if !os(tvOS) + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +final class PaywallWebViewValueTests: TestCase { + + func testFactoryAccessorsAndCodableRoundTrip() throws { + let value: PaywallWebViewValue = .object([ + "string": .string("value"), + "number": .number(1.25), + "bool": .bool(true), + "array": .array([.null, .string("x")]), + "object": .object(["nested": .bool(false)]), + "null": .null + ]) + + let decoded = try JSONDecoder().decode( + PaywallWebViewValue.self, + from: try JSONEncoder().encode(value) + ) + + XCTAssertEqual(decoded, value) + XCTAssertEqual(decoded.objectValue?["string"]?.stringValue, "value") + XCTAssertEqual(decoded.objectValue?["number"]?.numberValue, 1.25) + XCTAssertEqual(decoded.objectValue?["bool"]?.boolValue, true) + XCTAssertTrue(decoded.objectValue?["null"]?.isNull == true) + } + + func testNumberBoolDisambiguation() throws { + let decoded = try JSONDecoder().decode( + PaywallWebViewValue.self, + from: Data(#"{"bool":true,"number":1}"#.utf8) + ) + + XCTAssertEqual(decoded.objectValue?["bool"]?.boolValue, true) + XCTAssertNil(decoded.objectValue?["bool"]?.numberValue) + XCTAssertEqual(decoded.objectValue?["number"]?.numberValue, 1) + } + + func testNonFiniteNumbersNormalizeToNullAndEncode() throws { + XCTAssertTrue(PaywallWebViewValue.number(.nan).isNull) + XCTAssertTrue(PaywallWebViewValue.number(.infinity).isNull) + XCTAssertTrue(PaywallWebViewValue.number(-.infinity).isNull) + XCTAssertNil(PaywallWebViewValue.number(.nan).numberValue) + + let value: PaywallWebViewValue = .object(["bad": .number(.nan)]) + XCTAssertNoThrow(try JSONEncoder().encode(value)) + XCTAssertEqual(PaywallWebViewValue.number(-0.0), .number(0.0)) + } + +} + +#endif diff --git a/api/revenuecatui-api-ios-simulator.swiftinterface b/api/revenuecatui-api-ios-simulator.swiftinterface index 30bbb7549c..428cfba3b0 100644 --- a/api/revenuecatui-api-ios-simulator.swiftinterface +++ b/api/revenuecatui-api-ios-simulator.swiftinterface @@ -309,6 +309,66 @@ extension RevenueCat.Purchases { @available(iOS 15.0, macOS 12.0, *) @_Concurrency.MainActor @objc final public func presentPaywall(from url: Foundation.URL) -> Swift.Bool } +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +public struct PaywallWebViewMessage : Swift.Sendable, Swift.Equatable { + public let componentID: Swift.String + public let type: Swift.String + public let responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? + public let error: Swift.String? + public init(componentID: Swift.String, type: Swift.String, responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? = nil, error: Swift.String? = nil) + public static func == (a: RevenueCatUI.PaywallWebViewMessage, b: RevenueCatUI.PaywallWebViewMessage) -> Swift.Bool +} +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +public struct PaywallWebViewValue : Swift.Sendable, Swift.Equatable, Swift.Hashable, Swift.Codable { + public static func string(_ value: Swift.String) -> RevenueCatUI.PaywallWebViewValue + public static func number(_ value: Swift.Double) -> RevenueCatUI.PaywallWebViewValue + public static func bool(_ value: Swift.Bool) -> RevenueCatUI.PaywallWebViewValue + public static func array(_ value: [RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue + public static func object(_ value: [Swift.String : RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue + public static var null: RevenueCatUI.PaywallWebViewValue { + get + } + public var stringValue: Swift.String? { + get + } + public var numberValue: Swift.Double? { + get + } + public var boolValue: Swift.Bool? { + get + } + public var arrayValue: [RevenueCatUI.PaywallWebViewValue]? { + get + } + public var objectValue: [Swift.String : RevenueCatUI.PaywallWebViewValue]? { + get + } + public var isNull: Swift.Bool { + get + } + public init(from decoder: any Swift.Decoder) throws + public func encode(to encoder: any Swift.Encoder) throws + public static func == (a: RevenueCatUI.PaywallWebViewValue, b: RevenueCatUI.PaywallWebViewValue) -> Swift.Bool + public func hash(into hasher: inout Swift.Hasher) + public var hashValue: Swift.Int { + get + } +} +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@_Concurrency.MainActor public struct PaywallWebViewController { + @_Concurrency.MainActor public func postVariables(componentID: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) + @_Concurrency.MainActor public func postMessage(componentID: Swift.String, type: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) +} +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +public struct PaywallWebViewMessageAction { + public init(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) + @_Concurrency.MainActor public func callAsFunction(_ message: RevenueCatUI.PaywallWebViewMessage, _ controller: RevenueCatUI.PaywallWebViewController) +} +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +extension SwiftUICore.View { + @_Concurrency.MainActor @preconcurrency public func onPaywallWebViewMessage(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) -> some SwiftUICore.View + +} @_inheritsConvenienceInitializers @_hasMissingDesignatedInitializers @available(iOS 15.0, macOS 12.0, tvOS 15.0, *) @objc(RCPaywallFooterViewController) @_Concurrency.MainActor @preconcurrency final public class PaywallFooterViewController : RevenueCatUI.PaywallViewController { @_Concurrency.MainActor @preconcurrency public init(offering: RevenueCat.Offering? = nil, performPurchase: @escaping RevenueCatUI.PerformPurchase, performRestore: @escaping RevenueCatUI.PerformRestore, dismissRequestedHandler: ((_ controller: RevenueCatUI.PaywallViewController) -> Swift.Void)? = nil) @@ -546,5 +606,7 @@ extension RevenueCatUI.CustomerCenterNavigationLink : Swift.Sendable {} @available(iOS 15.0, macOS 12.0, watchOS 8.0, *) @available(tvOS, unavailable, message: "RevenueCatUI does not support tvOS yet") extension RevenueCatUI.PaywallView : Swift.Sendable {} +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +extension RevenueCatUI.PaywallWebViewController : Swift.Sendable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Equatable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Hashable {} diff --git a/api/revenuecatui-api-ios.swiftinterface b/api/revenuecatui-api-ios.swiftinterface index 5106fe465c..9514729189 100644 --- a/api/revenuecatui-api-ios.swiftinterface +++ b/api/revenuecatui-api-ios.swiftinterface @@ -309,6 +309,66 @@ extension RevenueCat.Purchases { @available(iOS 15.0, macOS 12.0, *) @_Concurrency.MainActor @objc final public func presentPaywall(from url: Foundation.URL) -> Swift.Bool } +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +public struct PaywallWebViewMessage : Swift.Sendable, Swift.Equatable { + public let componentID: Swift.String + public let type: Swift.String + public let responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? + public let error: Swift.String? + public init(componentID: Swift.String, type: Swift.String, responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? = nil, error: Swift.String? = nil) + public static func == (a: RevenueCatUI.PaywallWebViewMessage, b: RevenueCatUI.PaywallWebViewMessage) -> Swift.Bool +} +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +public struct PaywallWebViewValue : Swift.Sendable, Swift.Equatable, Swift.Hashable, Swift.Codable { + public static func string(_ value: Swift.String) -> RevenueCatUI.PaywallWebViewValue + public static func number(_ value: Swift.Double) -> RevenueCatUI.PaywallWebViewValue + public static func bool(_ value: Swift.Bool) -> RevenueCatUI.PaywallWebViewValue + public static func array(_ value: [RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue + public static func object(_ value: [Swift.String : RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue + public static var null: RevenueCatUI.PaywallWebViewValue { + get + } + public var stringValue: Swift.String? { + get + } + public var numberValue: Swift.Double? { + get + } + public var boolValue: Swift.Bool? { + get + } + public var arrayValue: [RevenueCatUI.PaywallWebViewValue]? { + get + } + public var objectValue: [Swift.String : RevenueCatUI.PaywallWebViewValue]? { + get + } + public var isNull: Swift.Bool { + get + } + public init(from decoder: any Swift.Decoder) throws + public func encode(to encoder: any Swift.Encoder) throws + public static func == (a: RevenueCatUI.PaywallWebViewValue, b: RevenueCatUI.PaywallWebViewValue) -> Swift.Bool + public func hash(into hasher: inout Swift.Hasher) + public var hashValue: Swift.Int { + get + } +} +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@_Concurrency.MainActor public struct PaywallWebViewController { + @_Concurrency.MainActor public func postVariables(componentID: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) + @_Concurrency.MainActor public func postMessage(componentID: Swift.String, type: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) +} +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +public struct PaywallWebViewMessageAction { + public init(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) + @_Concurrency.MainActor public func callAsFunction(_ message: RevenueCatUI.PaywallWebViewMessage, _ controller: RevenueCatUI.PaywallWebViewController) +} +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +extension SwiftUICore.View { + @_Concurrency.MainActor @preconcurrency public func onPaywallWebViewMessage(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) -> some SwiftUICore.View + +} @_inheritsConvenienceInitializers @_hasMissingDesignatedInitializers @available(iOS 15.0, macOS 12.0, tvOS 15.0, *) @objc(RCPaywallFooterViewController) @_Concurrency.MainActor @preconcurrency final public class PaywallFooterViewController : RevenueCatUI.PaywallViewController { @_Concurrency.MainActor @preconcurrency public init(offering: RevenueCat.Offering? = nil, performPurchase: @escaping RevenueCatUI.PerformPurchase, performRestore: @escaping RevenueCatUI.PerformRestore, dismissRequestedHandler: ((_ controller: RevenueCatUI.PaywallViewController) -> Swift.Void)? = nil) @@ -546,5 +606,7 @@ extension RevenueCatUI.CustomerCenterNavigationLink : Swift.Sendable {} @available(iOS 15.0, macOS 12.0, watchOS 8.0, *) @available(tvOS, unavailable, message: "RevenueCatUI does not support tvOS yet") extension RevenueCatUI.PaywallView : Swift.Sendable {} +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +extension RevenueCatUI.PaywallWebViewController : Swift.Sendable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Equatable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Hashable {} diff --git a/api/revenuecatui-api-macos.swiftinterface b/api/revenuecatui-api-macos.swiftinterface index e64db71c52..d7553c3e37 100644 --- a/api/revenuecatui-api-macos.swiftinterface +++ b/api/revenuecatui-api-macos.swiftinterface @@ -168,6 +168,66 @@ open class CustomPaywallFontProvider : RevenueCatUI.PaywallFontProvider { @available(tvOS, unavailable, introduced: 15.0, message: "RevenueCatUI does not support tvOS yet") public typealias Body = @_opaqueReturnTypeOf("$s12RevenueCatUI11PaywallViewV4bodyQrvp", 0) __ } +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +public struct PaywallWebViewMessage : Swift.Sendable, Swift.Equatable { + public let componentID: Swift.String + public let type: Swift.String + public let responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? + public let error: Swift.String? + public init(componentID: Swift.String, type: Swift.String, responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? = nil, error: Swift.String? = nil) + public static func == (a: RevenueCatUI.PaywallWebViewMessage, b: RevenueCatUI.PaywallWebViewMessage) -> Swift.Bool +} +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +public struct PaywallWebViewValue : Swift.Sendable, Swift.Equatable, Swift.Hashable, Swift.Codable { + public static func string(_ value: Swift.String) -> RevenueCatUI.PaywallWebViewValue + public static func number(_ value: Swift.Double) -> RevenueCatUI.PaywallWebViewValue + public static func bool(_ value: Swift.Bool) -> RevenueCatUI.PaywallWebViewValue + public static func array(_ value: [RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue + public static func object(_ value: [Swift.String : RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue + public static var null: RevenueCatUI.PaywallWebViewValue { + get + } + public var stringValue: Swift.String? { + get + } + public var numberValue: Swift.Double? { + get + } + public var boolValue: Swift.Bool? { + get + } + public var arrayValue: [RevenueCatUI.PaywallWebViewValue]? { + get + } + public var objectValue: [Swift.String : RevenueCatUI.PaywallWebViewValue]? { + get + } + public var isNull: Swift.Bool { + get + } + public init(from decoder: any Swift.Decoder) throws + public func encode(to encoder: any Swift.Encoder) throws + public static func == (a: RevenueCatUI.PaywallWebViewValue, b: RevenueCatUI.PaywallWebViewValue) -> Swift.Bool + public func hash(into hasher: inout Swift.Hasher) + public var hashValue: Swift.Int { + get + } +} +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@_Concurrency.MainActor public struct PaywallWebViewController { + @_Concurrency.MainActor public func postVariables(componentID: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) + @_Concurrency.MainActor public func postMessage(componentID: Swift.String, type: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) +} +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +public struct PaywallWebViewMessageAction { + public init(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) + @_Concurrency.MainActor public func callAsFunction(_ message: RevenueCatUI.PaywallWebViewMessage, _ controller: RevenueCatUI.PaywallWebViewController) +} +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +extension SwiftUICore.View { + @_Concurrency.MainActor @preconcurrency public func onPaywallWebViewMessage(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) -> some SwiftUICore.View + +} @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) extension SwiftUICore.View { @_Concurrency.MainActor @preconcurrency public func onWebPurchaseRedemptionAttempt(perform completion: @escaping @Sendable (RevenueCat.WebPurchaseRedemptionResult) -> Swift.Void) -> some SwiftUICore.View @@ -324,5 +384,7 @@ extension SwiftUICore.View { @available(iOS 15.0, macOS 12.0, watchOS 8.0, *) @available(tvOS, unavailable, message: "RevenueCatUI does not support tvOS yet") extension RevenueCatUI.PaywallView : Swift.Sendable {} +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +extension RevenueCatUI.PaywallWebViewController : Swift.Sendable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Equatable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Hashable {} diff --git a/api/revenuecatui-api-visionos-simulator.swiftinterface b/api/revenuecatui-api-visionos-simulator.swiftinterface index b87d2b82d9..7369428ec5 100644 --- a/api/revenuecatui-api-visionos-simulator.swiftinterface +++ b/api/revenuecatui-api-visionos-simulator.swiftinterface @@ -179,6 +179,66 @@ extension RevenueCat.Purchases { @available(iOS 15.0, macOS 12.0, *) @_Concurrency.MainActor @objc final public func presentPaywall(from url: Foundation.URL) -> Swift.Bool } +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +public struct PaywallWebViewMessage : Swift.Sendable, Swift.Equatable { + public let componentID: Swift.String + public let type: Swift.String + public let responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? + public let error: Swift.String? + public init(componentID: Swift.String, type: Swift.String, responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? = nil, error: Swift.String? = nil) + public static func == (a: RevenueCatUI.PaywallWebViewMessage, b: RevenueCatUI.PaywallWebViewMessage) -> Swift.Bool +} +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +public struct PaywallWebViewValue : Swift.Sendable, Swift.Equatable, Swift.Hashable, Swift.Codable { + public static func string(_ value: Swift.String) -> RevenueCatUI.PaywallWebViewValue + public static func number(_ value: Swift.Double) -> RevenueCatUI.PaywallWebViewValue + public static func bool(_ value: Swift.Bool) -> RevenueCatUI.PaywallWebViewValue + public static func array(_ value: [RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue + public static func object(_ value: [Swift.String : RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue + public static var null: RevenueCatUI.PaywallWebViewValue { + get + } + public var stringValue: Swift.String? { + get + } + public var numberValue: Swift.Double? { + get + } + public var boolValue: Swift.Bool? { + get + } + public var arrayValue: [RevenueCatUI.PaywallWebViewValue]? { + get + } + public var objectValue: [Swift.String : RevenueCatUI.PaywallWebViewValue]? { + get + } + public var isNull: Swift.Bool { + get + } + public init(from decoder: any Swift.Decoder) throws + public func encode(to encoder: any Swift.Encoder) throws + public static func == (a: RevenueCatUI.PaywallWebViewValue, b: RevenueCatUI.PaywallWebViewValue) -> Swift.Bool + public func hash(into hasher: inout Swift.Hasher) + public var hashValue: Swift.Int { + get + } +} +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@_Concurrency.MainActor public struct PaywallWebViewController { + @_Concurrency.MainActor public func postVariables(componentID: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) + @_Concurrency.MainActor public func postMessage(componentID: Swift.String, type: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) +} +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +public struct PaywallWebViewMessageAction { + public init(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) + @_Concurrency.MainActor public func callAsFunction(_ message: RevenueCatUI.PaywallWebViewMessage, _ controller: RevenueCatUI.PaywallWebViewController) +} +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +extension SwiftUICore.View { + @_Concurrency.MainActor @preconcurrency public func onPaywallWebViewMessage(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) -> some SwiftUICore.View + +} @_inheritsConvenienceInitializers @_hasMissingDesignatedInitializers @available(iOS 15.0, macOS 12.0, tvOS 15.0, *) @objc(RCPaywallFooterViewController) @_Concurrency.MainActor @preconcurrency final public class PaywallFooterViewController : RevenueCatUI.PaywallViewController { @_Concurrency.MainActor @preconcurrency public init(offering: RevenueCat.Offering? = nil, performPurchase: @escaping RevenueCatUI.PerformPurchase, performRestore: @escaping RevenueCatUI.PerformRestore, dismissRequestedHandler: ((_ controller: RevenueCatUI.PaywallViewController) -> Swift.Void)? = nil) @@ -406,5 +466,7 @@ extension SwiftUICore.View { @available(iOS 15.0, macOS 12.0, watchOS 8.0, *) @available(tvOS, unavailable, message: "RevenueCatUI does not support tvOS yet") extension RevenueCatUI.PaywallView : Swift.Sendable {} +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +extension RevenueCatUI.PaywallWebViewController : Swift.Sendable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Equatable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Hashable {} diff --git a/api/revenuecatui-api-visionos.swiftinterface b/api/revenuecatui-api-visionos.swiftinterface index ec8d35ca1f..cccba66c64 100644 --- a/api/revenuecatui-api-visionos.swiftinterface +++ b/api/revenuecatui-api-visionos.swiftinterface @@ -179,6 +179,66 @@ extension RevenueCat.Purchases { @available(iOS 15.0, macOS 12.0, *) @_Concurrency.MainActor @objc final public func presentPaywall(from url: Foundation.URL) -> Swift.Bool } +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +public struct PaywallWebViewMessage : Swift.Sendable, Swift.Equatable { + public let componentID: Swift.String + public let type: Swift.String + public let responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? + public let error: Swift.String? + public init(componentID: Swift.String, type: Swift.String, responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? = nil, error: Swift.String? = nil) + public static func == (a: RevenueCatUI.PaywallWebViewMessage, b: RevenueCatUI.PaywallWebViewMessage) -> Swift.Bool +} +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +public struct PaywallWebViewValue : Swift.Sendable, Swift.Equatable, Swift.Hashable, Swift.Codable { + public static func string(_ value: Swift.String) -> RevenueCatUI.PaywallWebViewValue + public static func number(_ value: Swift.Double) -> RevenueCatUI.PaywallWebViewValue + public static func bool(_ value: Swift.Bool) -> RevenueCatUI.PaywallWebViewValue + public static func array(_ value: [RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue + public static func object(_ value: [Swift.String : RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue + public static var null: RevenueCatUI.PaywallWebViewValue { + get + } + public var stringValue: Swift.String? { + get + } + public var numberValue: Swift.Double? { + get + } + public var boolValue: Swift.Bool? { + get + } + public var arrayValue: [RevenueCatUI.PaywallWebViewValue]? { + get + } + public var objectValue: [Swift.String : RevenueCatUI.PaywallWebViewValue]? { + get + } + public var isNull: Swift.Bool { + get + } + public init(from decoder: any Swift.Decoder) throws + public func encode(to encoder: any Swift.Encoder) throws + public static func == (a: RevenueCatUI.PaywallWebViewValue, b: RevenueCatUI.PaywallWebViewValue) -> Swift.Bool + public func hash(into hasher: inout Swift.Hasher) + public var hashValue: Swift.Int { + get + } +} +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@_Concurrency.MainActor public struct PaywallWebViewController { + @_Concurrency.MainActor public func postVariables(componentID: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) + @_Concurrency.MainActor public func postMessage(componentID: Swift.String, type: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) +} +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +public struct PaywallWebViewMessageAction { + public init(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) + @_Concurrency.MainActor public func callAsFunction(_ message: RevenueCatUI.PaywallWebViewMessage, _ controller: RevenueCatUI.PaywallWebViewController) +} +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +extension SwiftUICore.View { + @_Concurrency.MainActor @preconcurrency public func onPaywallWebViewMessage(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) -> some SwiftUICore.View + +} @_inheritsConvenienceInitializers @_hasMissingDesignatedInitializers @available(iOS 15.0, macOS 12.0, tvOS 15.0, *) @objc(RCPaywallFooterViewController) @_Concurrency.MainActor @preconcurrency final public class PaywallFooterViewController : RevenueCatUI.PaywallViewController { @_Concurrency.MainActor @preconcurrency public init(offering: RevenueCat.Offering? = nil, performPurchase: @escaping RevenueCatUI.PerformPurchase, performRestore: @escaping RevenueCatUI.PerformRestore, dismissRequestedHandler: ((_ controller: RevenueCatUI.PaywallViewController) -> Swift.Void)? = nil) @@ -406,5 +466,7 @@ extension SwiftUICore.View { @available(iOS 15.0, macOS 12.0, watchOS 8.0, *) @available(tvOS, unavailable, message: "RevenueCatUI does not support tvOS yet") extension RevenueCatUI.PaywallView : Swift.Sendable {} +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +extension RevenueCatUI.PaywallWebViewController : Swift.Sendable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Equatable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Hashable {} diff --git a/api/revenuecatui-api-watchos-simulator.swiftinterface b/api/revenuecatui-api-watchos-simulator.swiftinterface index 03d128c761..7038282c5d 100644 --- a/api/revenuecatui-api-watchos-simulator.swiftinterface +++ b/api/revenuecatui-api-watchos-simulator.swiftinterface @@ -166,6 +166,66 @@ open class CustomPaywallFontProvider : RevenueCatUI.PaywallFontProvider { @available(tvOS, unavailable, introduced: 15.0, message: "RevenueCatUI does not support tvOS yet") public typealias Body = @_opaqueReturnTypeOf("$s12RevenueCatUI11PaywallViewV4bodyQrvp", 0) __ } +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +public struct PaywallWebViewMessage : Swift.Sendable, Swift.Equatable { + public let componentID: Swift.String + public let type: Swift.String + public let responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? + public let error: Swift.String? + public init(componentID: Swift.String, type: Swift.String, responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? = nil, error: Swift.String? = nil) + public static func == (a: RevenueCatUI.PaywallWebViewMessage, b: RevenueCatUI.PaywallWebViewMessage) -> Swift.Bool +} +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +public struct PaywallWebViewValue : Swift.Sendable, Swift.Equatable, Swift.Hashable, Swift.Codable { + public static func string(_ value: Swift.String) -> RevenueCatUI.PaywallWebViewValue + public static func number(_ value: Swift.Double) -> RevenueCatUI.PaywallWebViewValue + public static func bool(_ value: Swift.Bool) -> RevenueCatUI.PaywallWebViewValue + public static func array(_ value: [RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue + public static func object(_ value: [Swift.String : RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue + public static var null: RevenueCatUI.PaywallWebViewValue { + get + } + public var stringValue: Swift.String? { + get + } + public var numberValue: Swift.Double? { + get + } + public var boolValue: Swift.Bool? { + get + } + public var arrayValue: [RevenueCatUI.PaywallWebViewValue]? { + get + } + public var objectValue: [Swift.String : RevenueCatUI.PaywallWebViewValue]? { + get + } + public var isNull: Swift.Bool { + get + } + public init(from decoder: any Swift.Decoder) throws + public func encode(to encoder: any Swift.Encoder) throws + public static func == (a: RevenueCatUI.PaywallWebViewValue, b: RevenueCatUI.PaywallWebViewValue) -> Swift.Bool + public func hash(into hasher: inout Swift.Hasher) + public var hashValue: Swift.Int { + get + } +} +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@_Concurrency.MainActor public struct PaywallWebViewController { + @_Concurrency.MainActor public func postVariables(componentID: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) + @_Concurrency.MainActor public func postMessage(componentID: Swift.String, type: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) +} +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +public struct PaywallWebViewMessageAction { + public init(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) + @_Concurrency.MainActor public func callAsFunction(_ message: RevenueCatUI.PaywallWebViewMessage, _ controller: RevenueCatUI.PaywallWebViewController) +} +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +extension SwiftUICore.View { + @_Concurrency.MainActor @preconcurrency public func onPaywallWebViewMessage(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) -> some SwiftUICore.View + +} @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) extension SwiftUICore.View { @_Concurrency.MainActor @preconcurrency public func onWebPurchaseRedemptionAttempt(perform completion: @escaping @Sendable (RevenueCat.WebPurchaseRedemptionResult) -> Swift.Void) -> some SwiftUICore.View @@ -279,5 +339,7 @@ extension SwiftUICore.View { @available(iOS 15.0, macOS 12.0, watchOS 8.0, *) @available(tvOS, unavailable, message: "RevenueCatUI does not support tvOS yet") extension RevenueCatUI.PaywallView : Swift.Sendable {} +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +extension RevenueCatUI.PaywallWebViewController : Swift.Sendable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Equatable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Hashable {} diff --git a/api/revenuecatui-api-watchos.swiftinterface b/api/revenuecatui-api-watchos.swiftinterface index 39b2ed3084..0490cf9e59 100644 --- a/api/revenuecatui-api-watchos.swiftinterface +++ b/api/revenuecatui-api-watchos.swiftinterface @@ -166,6 +166,66 @@ open class CustomPaywallFontProvider : RevenueCatUI.PaywallFontProvider { @available(tvOS, unavailable, introduced: 15.0, message: "RevenueCatUI does not support tvOS yet") public typealias Body = @_opaqueReturnTypeOf("$s12RevenueCatUI11PaywallViewV4bodyQrvp", 0) __ } +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +public struct PaywallWebViewMessage : Swift.Sendable, Swift.Equatable { + public let componentID: Swift.String + public let type: Swift.String + public let responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? + public let error: Swift.String? + public init(componentID: Swift.String, type: Swift.String, responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? = nil, error: Swift.String? = nil) + public static func == (a: RevenueCatUI.PaywallWebViewMessage, b: RevenueCatUI.PaywallWebViewMessage) -> Swift.Bool +} +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +public struct PaywallWebViewValue : Swift.Sendable, Swift.Equatable, Swift.Hashable, Swift.Codable { + public static func string(_ value: Swift.String) -> RevenueCatUI.PaywallWebViewValue + public static func number(_ value: Swift.Double) -> RevenueCatUI.PaywallWebViewValue + public static func bool(_ value: Swift.Bool) -> RevenueCatUI.PaywallWebViewValue + public static func array(_ value: [RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue + public static func object(_ value: [Swift.String : RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue + public static var null: RevenueCatUI.PaywallWebViewValue { + get + } + public var stringValue: Swift.String? { + get + } + public var numberValue: Swift.Double? { + get + } + public var boolValue: Swift.Bool? { + get + } + public var arrayValue: [RevenueCatUI.PaywallWebViewValue]? { + get + } + public var objectValue: [Swift.String : RevenueCatUI.PaywallWebViewValue]? { + get + } + public var isNull: Swift.Bool { + get + } + public init(from decoder: any Swift.Decoder) throws + public func encode(to encoder: any Swift.Encoder) throws + public static func == (a: RevenueCatUI.PaywallWebViewValue, b: RevenueCatUI.PaywallWebViewValue) -> Swift.Bool + public func hash(into hasher: inout Swift.Hasher) + public var hashValue: Swift.Int { + get + } +} +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@_Concurrency.MainActor public struct PaywallWebViewController { + @_Concurrency.MainActor public func postVariables(componentID: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) + @_Concurrency.MainActor public func postMessage(componentID: Swift.String, type: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) +} +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +public struct PaywallWebViewMessageAction { + public init(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) + @_Concurrency.MainActor public func callAsFunction(_ message: RevenueCatUI.PaywallWebViewMessage, _ controller: RevenueCatUI.PaywallWebViewController) +} +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +extension SwiftUICore.View { + @_Concurrency.MainActor @preconcurrency public func onPaywallWebViewMessage(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) -> some SwiftUICore.View + +} @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) extension SwiftUICore.View { @_Concurrency.MainActor @preconcurrency public func onWebPurchaseRedemptionAttempt(perform completion: @escaping @Sendable (RevenueCat.WebPurchaseRedemptionResult) -> Swift.Void) -> some SwiftUICore.View @@ -279,5 +339,7 @@ extension SwiftUICore.View { @available(iOS 15.0, macOS 12.0, watchOS 8.0, *) @available(tvOS, unavailable, message: "RevenueCatUI does not support tvOS yet") extension RevenueCatUI.PaywallView : Swift.Sendable {} +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +extension RevenueCatUI.PaywallWebViewController : Swift.Sendable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Equatable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Hashable {} From f7f5d16f422944c7691068048844e509caaca0f7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 15 Jul 2026 20:29:47 +0000 Subject: [PATCH 02/13] feat(paywalls): web_view wire envelope + JSON hardening (2/7) Add WebViewEnvelope encode/decode and input hardening tests. Inert until Part 7; builds on the public API from Part 1. Co-authored-by: Jacob Rakidzich --- RevenueCat.xcodeproj/project.pbxproj | 6 + .../Components/WebView/WebViewEnvelope.swift | 176 ++++++++++++++++++ .../PaywallsV2/WebViewEnvelopeTests.swift | 135 ++++++++++++++ 3 files changed, 317 insertions(+) create mode 100644 RevenueCatUI/Templates/V2/Components/WebView/WebViewEnvelope.swift create mode 100644 Tests/RevenueCatUITests/PaywallsV2/WebViewEnvelopeTests.swift diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 0f4bd8f8c2..0c85a7d13c 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -108,6 +108,7 @@ 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 */; }; + 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 */; }; @@ -1638,6 +1639,8 @@ 16DA8EDE2E4F6A6600283940 /* PaywallVideoComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallVideoComponent.swift; sourceTree = ""; }; 16DA8EF32E4F7A2500283940 /* VideoComponentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoComponentTests.swift; sourceTree = ""; }; 9D0C57ECDF0E7F1A5B4418FF /* PaywallWebViewAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallWebViewAPI.swift; sourceTree = ""; }; + 89DD761C9E00E61032AAEB44 /* WebViewEnvelope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewEnvelope.swift; sourceTree = ""; }; + C1B939C7DAAFEA1DE2DE2674 /* WebViewEnvelopeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewEnvelopeTests.swift; sourceTree = ""; }; 686562C08D953A27986DB276 /* PaywallWebViewValueTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallWebViewValueTests.swift; sourceTree = ""; }; 16DA8F1B2E4FB6E200283940 /* ImageComponent.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = ImageComponent.json; sourceTree = ""; }; 16DA8F1C2E4FB6E200283940 /* VideoComponent.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = VideoComponent.json; sourceTree = ""; }; @@ -3169,6 +3172,7 @@ isa = PBXGroup; children = ( 686562C08D953A27986DB276 /* PaywallWebViewValueTests.swift */, + C1B939C7DAAFEA1DE2DE2674 /* WebViewEnvelopeTests.swift */, 802593842FE07057005AF6DF /* PaywallStateStoreTests.swift */, 16A9F7B62FAA22D0008E8A4D /* VideoComponentViewTests.swift */, A9D45E4EAFAD4C0385BEDFB3 /* VideoPlayerViewTests.swift */, @@ -3305,6 +3309,7 @@ isa = PBXGroup; children = ( 9D0C57ECDF0E7F1A5B4418FF /* PaywallWebViewAPI.swift */, + 89DD761C9E00E61032AAEB44 /* WebViewEnvelope.swift */, ); path = WebView; sourceTree = ""; @@ -8247,6 +8252,7 @@ 164681D12E6B577600854AA5 /* VideoPlayerViewUIView.swift in Sources */, 164681D62E6B577600854AA5 /* VideoPlayerLayerUIView.swift in Sources */, FA4A2DF41FC434A9401F08CC /* PaywallWebViewAPI.swift in Sources */, + 11CC2EA83C56933E40D8E5D8 /* WebViewEnvelope.swift in Sources */, DBAA1FC22F8D4ED8000E8C81 /* HeaderNestedHeroZLayerSafeAreaPreview.swift in Sources */, 164681D22E6B577600854AA5 /* VideoComponentViewModel.swift in Sources */, 2C7457422CE81107004ACE52 /* IntroOfferEligibilityContext.swift in Sources */, diff --git a/RevenueCatUI/Templates/V2/Components/WebView/WebViewEnvelope.swift b/RevenueCatUI/Templates/V2/Components/WebView/WebViewEnvelope.swift new file mode 100644 index 0000000000..7905029df5 --- /dev/null +++ b/RevenueCatUI/Templates/V2/Components/WebView/WebViewEnvelope.swift @@ -0,0 +1,176 @@ +import Foundation +// swiftlint:disable nesting + +#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" + static let messageTypeResize = "resize" + static let messageTypeFit = "fit" + + static let maxInboundFrameBytes = 65_536 + static let maxJSONDepth = 16 + /// Structural depth budget for a whole inbound frame: the envelope object itself (1 level) + /// plus a payload tree of at most `maxJSONDepth` levels. + static let maxFrameDepth = maxJSONDepth + 1 + static let maxResizePoints: CGFloat = 10_000 + static let resizeThreshold: CGFloat = 1 + static let fallbackFitHeight: CGFloat = 100 + static let fallbackFitWidth: CGFloat = 300 + + enum Kind: String, Codable { + case connect + case `init` + case reject + case message + case request + case response + case error + } + + 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 + } + } + + static func decode(rawMessage: Any) -> Envelope? { + let data: Data + if let string = rawMessage as? String { + guard string.utf8.count <= Self.maxInboundFrameBytes, + let stringData = string.data(using: .utf8) else { + return nil + } + data = stringData + } else if let dictionary = rawMessage as? [String: Any] { + guard JSONSerialization.isValidJSONObject(dictionary), + let serialized = try? JSONSerialization.data(withJSONObject: dictionary), + serialized.count <= Self.maxInboundFrameBytes else { + return nil + } + data = serialized + } else { + return nil + } + + // Enforce the nesting limit BEFORE decoding: `PaywallWebViewValue.init(from:)` recurses + // per nesting level, so a hostile deeply-nested frame (tens of thousands of levels fit in + // 64 KiB) could otherwise overflow the stack before any post-decode check runs. + guard !Self.exceedsMaxDepth(data) else { + return nil + } + + guard let envelope = try? JSONDecoder().decode(Envelope.self, from: data), + envelope.channel == Self.channel else { + return nil + } + + return envelope + } + + /// Non-recursive scan of the raw JSON bytes, tracking `{`/`[` nesting outside string + /// literals (honoring `\` escapes). Returns `true` when the depth exceeds ``maxFrameDepth``. + static func exceedsMaxDepth(_ data: Data) -> Bool { + var depth = 0 + var inString = false + var escaped = false + + for byte in data { + if inString { + if escaped { + escaped = false + } else if byte == UInt8(ascii: "\\") { + escaped = true + } else if byte == UInt8(ascii: "\"") { + inString = false + } + continue + } + + switch byte { + case UInt8(ascii: "\""): + inString = true + case UInt8(ascii: "{"), UInt8(ascii: "["): + depth += 1 + if depth > Self.maxFrameDepth { + return true + } + case UInt8(ascii: "}"), UInt8(ascii: "]"): + depth -= 1 + default: + break + } + } + + return false + } + + static func receiveScript(for envelope: Envelope) -> String? { + 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);}})(); + """ + } + + static let reservedPayloadKeys: Set = [ + "channel", "protocol_version", "kind", "type", "component_id", "id", "error", "variables" + ] + +} + +#endif diff --git a/Tests/RevenueCatUITests/PaywallsV2/WebViewEnvelopeTests.swift b/Tests/RevenueCatUITests/PaywallsV2/WebViewEnvelopeTests.swift new file mode 100644 index 0000000000..509b97964e --- /dev/null +++ b/Tests/RevenueCatUITests/PaywallsV2/WebViewEnvelopeTests.swift @@ -0,0 +1,135 @@ +// +// Copyright RevenueCat Inc. All Rights Reserved. +// + +@testable import RevenueCatUI +import XCTest +// swiftlint:disable force_try + +#if !os(tvOS) + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +final class WebViewEnvelopeTests: TestCase { + + func testDecodesValidConnectFrame() throws { + let envelope = WebViewEnvelope.decode(rawMessage: Self.json([ + "channel": WebViewEnvelope.channel, + "protocol_version": 1, + "kind": "connect", + "component_id": "" + ])) + + XCTAssertEqual(envelope?.kind, .connect) + XCTAssertEqual(envelope?.protocolVersion, 1) + XCTAssertEqual(envelope?.componentID, "") + } + + func testRejectsMalformedFrames() { + XCTAssertNil(WebViewEnvelope.decode(rawMessage: Self.json([ + "channel": "wrong", + "protocol_version": 1, + "kind": "connect", + "component_id": "" + ]))) + XCTAssertNil(WebViewEnvelope.decode(rawMessage: Self.json([ + "channel": WebViewEnvelope.channel, + "protocol_version": 1, + "kind": "unknown", + "component_id": "" + ]))) + XCTAssertNil(WebViewEnvelope.decode(rawMessage: Self.json([ + "channel": WebViewEnvelope.channel, + "protocol_version": 1, + "kind": "message" + ]))) + XCTAssertNil(WebViewEnvelope.decode(rawMessage: "{not json")) + XCTAssertNil(WebViewEnvelope.decode(rawMessage: [ + "channel": WebViewEnvelope.channel, + "protocol_version": 1, + "kind": "message", + "component_id": "web", + "payload": ["nested": ["bad": Date()]] + ])) + } + + func testByteLimit() { + let underLimitPayload = String(repeating: "x", count: WebViewEnvelope.maxInboundFrameBytes - 250) + XCTAssertNotNil(WebViewEnvelope.decode(rawMessage: Self.json([ + "channel": WebViewEnvelope.channel, + "protocol_version": 1, + "kind": "message", + "component_id": "web", + "type": "rc:step-loaded", + "payload": ["value": underLimitPayload] + ]))) + + let overLimitPayload = String(repeating: "x", count: WebViewEnvelope.maxInboundFrameBytes + 1) + XCTAssertNil(WebViewEnvelope.decode(rawMessage: Self.json([ + "channel": WebViewEnvelope.channel, + "protocol_version": 1, + "kind": "message", + "component_id": "web", + "type": "rc:step-loaded", + "payload": ["value": overLimitPayload] + ]))) + } + + func testDepthLimit() { + XCTAssertNotNil(WebViewEnvelope.decode( + rawMessage: Self.json(Self.frame(payload: Self.nestedObject(depth: 15))) + )) + XCTAssertNil(WebViewEnvelope.decode(rawMessage: Self.json(Self.frame(payload: Self.nestedObject(depth: 16))))) + } + + func testEncodeDecodeRoundTripAndEscaping() throws { + let hostile = "annual\" }); alert('xss'); //\n\\ end\u{2028}\u{2029}" + let envelope = WebViewEnvelope.Envelope( + kind: .message, + componentID: "web", + type: "rc:variables", + payload: ["value": .string(hostile)] + ) + + let script = try XCTUnwrap(WebViewEnvelope.receiveScript(for: envelope)) + XCTAssertFalse(script.contains("\u{2028}")) + XCTAssertFalse(script.contains("\u{2029}")) + + let decoded = try Self.decodeEnvelope(fromScript: script) + XCTAssertEqual(decoded, envelope) + XCTAssertEqual(decoded.payload?["value"]?.stringValue, hostile) + } + + private static func frame(payload: [String: Any]) -> [String: Any] { + [ + "channel": WebViewEnvelope.channel, + "protocol_version": 1, + "kind": "message", + "component_id": "web", + "type": "rc:step-loaded", + "payload": payload + ] + } + + private static func nestedObject(depth: Int) -> [String: Any] { + var value: [String: Any] = ["leaf": true] + for _ in 0.. String { + let data = try! JSONSerialization.data(withJSONObject: object) + return String(data: data, encoding: .utf8)! + } + + static func decodeEnvelope(fromScript script: String) throws -> WebViewEnvelope.Envelope { + let start = try XCTUnwrap(script.range(of: "var m=")?.upperBound) + let end = try XCTUnwrap(script.range(of: ";if", range: start.. Date: Thu, 16 Jul 2026 20:01:56 +0000 Subject: [PATCH 03/13] refactor(paywalls): keep web view bridge internal Co-authored-by: Jacob Rakidzich --- .../WebView/PaywallWebViewAPI.swift | 61 ++++++++------- .../AllAPITests.xcodeproj/project.pbxproj | 4 - .../PaywallWebViewAPI.swift | 77 ------------------- ...enuecatui-api-ios-simulator.swiftinterface | 62 --------------- api/revenuecatui-api-ios.swiftinterface | 62 --------------- api/revenuecatui-api-macos.swiftinterface | 62 --------------- ...atui-api-visionos-simulator.swiftinterface | 62 --------------- api/revenuecatui-api-visionos.swiftinterface | 62 --------------- ...catui-api-watchos-simulator.swiftinterface | 62 --------------- api/revenuecatui-api-watchos.swiftinterface | 62 --------------- 10 files changed, 30 insertions(+), 546 deletions(-) delete mode 100644 Tests/APITesters/AllAPITests/RevenueCatUISwiftAPITester/PaywallWebViewAPI.swift diff --git a/RevenueCatUI/Templates/V2/Components/WebView/PaywallWebViewAPI.swift b/RevenueCatUI/Templates/V2/Components/WebView/PaywallWebViewAPI.swift index 5ba2a3fe3b..4aed083814 100644 --- a/RevenueCatUI/Templates/V2/Components/WebView/PaywallWebViewAPI.swift +++ b/RevenueCatUI/Templates/V2/Components/WebView/PaywallWebViewAPI.swift @@ -1,26 +1,25 @@ import Foundation import SwiftUI -// swiftlint:disable missing_docs #if !os(tvOS) // For Paywalls V2 /// A validated message sent from a Paywalls V2 `web_view` component to your app. @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewMessage: Sendable, Equatable { +struct PaywallWebViewMessage: Sendable, Equatable { /// The identifier of the `web_view` component that produced this message. - public let componentID: String + let componentID: String /// The message type, e.g. `"rc:step-complete"`. - public let type: String + let type: String /// The responses collected by the web flow. Only populated for `"rc:step-complete"` messages. - public let responses: [String: PaywallWebViewValue]? + let responses: [String: PaywallWebViewValue]? /// The error reported by the web content. Only populated for `"rc:error"` messages. - public let error: String? + let error: String? - public init( + init( componentID: String, type: String, responses: [String: PaywallWebViewValue]? = nil, @@ -36,7 +35,7 @@ public struct PaywallWebViewMessage: Sendable, Equatable { /// A JSON-compatible value exchanged between a Paywalls V2 `web_view` component and your app. @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewValue: Sendable, Equatable, Hashable, Codable { +struct PaywallWebViewValue: Sendable, Equatable, Hashable, Codable { private indirect enum Storage: Sendable, Equatable, Hashable { case string(String) @@ -54,54 +53,54 @@ public struct PaywallWebViewValue: Sendable, Equatable, Hashable, Codable { } /// Creates a string value. - public static func string(_ value: String) -> Self { Self(.string(value)) } + static func string(_ value: String) -> Self { Self(.string(value)) } /// Creates a numeric value. Non-finite values normalize to ``null`` because JSON cannot encode them. - public static func number(_ value: Double) -> Self { value.isFinite ? Self(.number(value)) : .null } + static func number(_ value: Double) -> Self { value.isFinite ? Self(.number(value)) : .null } /// Creates a boolean value. - public static func bool(_ value: Bool) -> Self { Self(.bool(value)) } + static func bool(_ value: Bool) -> Self { Self(.bool(value)) } /// Creates an array value. - public static func array(_ value: [PaywallWebViewValue]) -> Self { Self(.array(value)) } + static func array(_ value: [PaywallWebViewValue]) -> Self { Self(.array(value)) } /// Creates an object (dictionary) value. - public static func object(_ value: [String: PaywallWebViewValue]) -> Self { Self(.object(value)) } + static func object(_ value: [String: PaywallWebViewValue]) -> Self { Self(.object(value)) } /// A null value. - public static var null: Self { Self(.null) } + static var null: Self { Self(.null) } - public var stringValue: String? { + var stringValue: String? { if case .string(let value) = self.storage { return value } return nil } - public var numberValue: Double? { + var numberValue: Double? { if case .number(let value) = self.storage { return value } return nil } - public var boolValue: Bool? { + var boolValue: Bool? { if case .bool(let value) = self.storage { return value } return nil } - public var arrayValue: [PaywallWebViewValue]? { + var arrayValue: [PaywallWebViewValue]? { if case .array(let value) = self.storage { return value } return nil } - public var objectValue: [String: PaywallWebViewValue]? { + var objectValue: [String: PaywallWebViewValue]? { if case .object(let value) = self.storage { return value } return nil } - public var isNull: Bool { + var isNull: Bool { if case .null = self.storage { return true } return false } - public init(from decoder: Decoder) throws { + init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() if container.decodeNil() { @@ -119,7 +118,7 @@ public struct PaywallWebViewValue: Sendable, Equatable, Hashable, Codable { } } - public func encode(to encoder: Encoder) throws { + func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() switch self.storage { @@ -142,34 +141,34 @@ public struct PaywallWebViewValue: Sendable, Equatable, Hashable, Codable { @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @MainActor -public struct PaywallWebViewController { +struct PaywallWebViewController { init() {} - public func postVariables(componentID: String, variables: [String: PaywallWebViewValue]) { - // Session wiring lands in a later PR; public API is inert until then. + func postVariables(componentID: String, variables: [String: PaywallWebViewValue]) { + // Session wiring lands in a later PR. } - public func postMessage(componentID: String, type: String, variables: [String: PaywallWebViewValue]) { - // Session wiring lands in a later PR; public API is inert until then. + func postMessage(componentID: String, type: String, variables: [String: PaywallWebViewValue]) { + // Session wiring lands in a later PR. } } /// A wrapper for the Paywalls V2 `web_view` message handler. @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewMessageAction { +struct PaywallWebViewMessageAction { private let action: @MainActor (PaywallWebViewMessage, PaywallWebViewController) -> Void - public init( + init( _ action: @escaping @MainActor (PaywallWebViewMessage, PaywallWebViewController) -> Void ) { self.action = action } @MainActor - public func callAsFunction(_ message: PaywallWebViewMessage, _ controller: PaywallWebViewController) { + func callAsFunction(_ message: PaywallWebViewMessage, _ controller: PaywallWebViewController) { self.action(message, controller) } @@ -194,7 +193,7 @@ extension EnvironmentValues { extension View { /// Invokes the given closure when a Paywalls V2 `web_view` component sends a message. - public func onPaywallWebViewMessage( + func onPaywallWebViewMessage( _ action: @escaping @MainActor (PaywallWebViewMessage, PaywallWebViewController) -> Void ) -> some View { self.environment(\.paywallWebViewMessageAction, PaywallWebViewMessageAction(action)) diff --git a/Tests/APITesters/AllAPITests/AllAPITests.xcodeproj/project.pbxproj b/Tests/APITesters/AllAPITests/AllAPITests.xcodeproj/project.pbxproj index 74a25fd257..9cc8116989 100644 --- a/Tests/APITesters/AllAPITests/AllAPITests.xcodeproj/project.pbxproj +++ b/Tests/APITesters/AllAPITests/AllAPITests.xcodeproj/project.pbxproj @@ -8,7 +8,6 @@ /* Begin PBXBuildFile section */ 1E8A60812CDCF29D0034ACF3 /* RedeemWebPurchasesAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E8A60802CDCF29D0034ACF3 /* RedeemWebPurchasesAPI.swift */; }; - B096F58208E0AA6582F34133 /* PaywallWebViewAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0DC08831A62751743D823FA /* PaywallWebViewAPI.swift */; }; 1EB697C22CD26218003000FC /* WebPurchaseRedemptionResultAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EB697C12CD26218003000FC /* WebPurchaseRedemptionResultAPI.swift */; }; 2D4C60E12C5AD1BC00A29FD2 /* AllAPITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2D4C60D82C5AD1BC00A29FD2 /* AllAPITests.framework */; }; 2D4C60E62C5AD1BC00A29FD2 /* AllAPITestsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D4C60E52C5AD1BC00A29FD2 /* AllAPITestsTests.swift */; }; @@ -229,7 +228,6 @@ /* Begin PBXFileReference section */ 1E8A60802CDCF29D0034ACF3 /* RedeemWebPurchasesAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedeemWebPurchasesAPI.swift; sourceTree = ""; }; - B0DC08831A62751743D823FA /* PaywallWebViewAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallWebViewAPI.swift; sourceTree = ""; }; 1EB697C12CD26218003000FC /* WebPurchaseRedemptionResultAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebPurchaseRedemptionResultAPI.swift; sourceTree = ""; }; 2D4C60D82C5AD1BC00A29FD2 /* AllAPITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AllAPITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 2D4C60DB2C5AD1BC00A29FD2 /* AllAPITests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AllAPITests.h; sourceTree = ""; }; @@ -647,7 +645,6 @@ children = ( FD33CD722D035876000D13A4 /* CustomerCenterViewControllerAPI.swift */, 2D4C622D2C5ADA6700A29FD2 /* PaywallViewAPI.swift */, - B0DC08831A62751743D823FA /* PaywallWebViewAPI.swift */, 1E8A60802CDCF29D0034ACF3 /* RedeemWebPurchasesAPI.swift */, 2D4C622B2C5ADA6700A29FD2 /* PaywallViewControllerAPI.swift */, 2D4C622C2C5ADA6700A29FD2 /* main.swift */, @@ -1122,7 +1119,6 @@ FDCC983A2D0A0FD500F5854B /* CustomerCenterViewAPI.swift in Sources */, 2D4C62302C5ADA6700A29FD2 /* PaywallViewControllerAPI.swift in Sources */, 1E8A60812CDCF29D0034ACF3 /* RedeemWebPurchasesAPI.swift in Sources */, - B096F58208E0AA6582F34133 /* PaywallWebViewAPI.swift in Sources */, 2D4C622E2C5ADA6700A29FD2 /* PaywallViewAPI.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Tests/APITesters/AllAPITests/RevenueCatUISwiftAPITester/PaywallWebViewAPI.swift b/Tests/APITesters/AllAPITests/RevenueCatUISwiftAPITester/PaywallWebViewAPI.swift deleted file mode 100644 index a8a2986ca9..0000000000 --- a/Tests/APITesters/AllAPITests/RevenueCatUISwiftAPITester/PaywallWebViewAPI.swift +++ /dev/null @@ -1,77 +0,0 @@ -// -// PaywallWebViewAPI.swift -// RevenueCatUISwiftAPITester -// -// Compile-time checks for the Paywalls V2 web_view public API surface. -// - -import RevenueCat -import RevenueCatUI -import SwiftUI - -#if !os(tvOS) // For Paywalls V2 - -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -struct PaywallWebViewApp: View { - - var body: some View { - Text("") - .onPaywallWebViewMessage { (message: PaywallWebViewMessage, controller: PaywallWebViewController) in - let componentID: String = message.componentID - let type: String = message.type - let responses: [String: PaywallWebViewValue]? = message.responses - let error: String? = message.error - - controller.postVariables( - componentID: componentID, - variables: ["custom": .object(["plan": .string("annual")])] - ) - controller.postMessage(componentID: componentID, type: type, variables: [:]) - - _ = (responses, error) - } - } - -} - -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -func checkPaywallWebViewMessageAPI() { - let message = PaywallWebViewMessage( - componentID: "", - type: "rc:step-complete", - responses: ["selected_plan": .string("annual")], - error: nil - ) - let sameMessage: Bool = message == message - _ = sameMessage -} - -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -func checkPaywallWebViewValueAPI() { - let string: PaywallWebViewValue = .string("s") - let number: PaywallWebViewValue = .number(1.5) - let bool: PaywallWebViewValue = .bool(true) - let array: PaywallWebViewValue = .array([string, number]) - let object: PaywallWebViewValue = .object(["key": bool]) - let null: PaywallWebViewValue = .null - - let _: String? = string.stringValue - let _: Double? = number.numberValue - let _: Bool? = bool.boolValue - let _: [PaywallWebViewValue]? = array.arrayValue - let _: [String: PaywallWebViewValue]? = object.objectValue - let _: Bool = null.isNull - let _: Set = [string, number] -} - -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -@MainActor -func checkPaywallWebViewMessageActionAPI( - message: PaywallWebViewMessage, - controller: PaywallWebViewController -) { - let action = PaywallWebViewMessageAction { (_: PaywallWebViewMessage, _: PaywallWebViewController) in } - action(message, controller) -} - -#endif diff --git a/api/revenuecatui-api-ios-simulator.swiftinterface b/api/revenuecatui-api-ios-simulator.swiftinterface index 428cfba3b0..30bbb7549c 100644 --- a/api/revenuecatui-api-ios-simulator.swiftinterface +++ b/api/revenuecatui-api-ios-simulator.swiftinterface @@ -309,66 +309,6 @@ extension RevenueCat.Purchases { @available(iOS 15.0, macOS 12.0, *) @_Concurrency.MainActor @objc final public func presentPaywall(from url: Foundation.URL) -> Swift.Bool } -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewMessage : Swift.Sendable, Swift.Equatable { - public let componentID: Swift.String - public let type: Swift.String - public let responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? - public let error: Swift.String? - public init(componentID: Swift.String, type: Swift.String, responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? = nil, error: Swift.String? = nil) - public static func == (a: RevenueCatUI.PaywallWebViewMessage, b: RevenueCatUI.PaywallWebViewMessage) -> Swift.Bool -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewValue : Swift.Sendable, Swift.Equatable, Swift.Hashable, Swift.Codable { - public static func string(_ value: Swift.String) -> RevenueCatUI.PaywallWebViewValue - public static func number(_ value: Swift.Double) -> RevenueCatUI.PaywallWebViewValue - public static func bool(_ value: Swift.Bool) -> RevenueCatUI.PaywallWebViewValue - public static func array(_ value: [RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue - public static func object(_ value: [Swift.String : RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue - public static var null: RevenueCatUI.PaywallWebViewValue { - get - } - public var stringValue: Swift.String? { - get - } - public var numberValue: Swift.Double? { - get - } - public var boolValue: Swift.Bool? { - get - } - public var arrayValue: [RevenueCatUI.PaywallWebViewValue]? { - get - } - public var objectValue: [Swift.String : RevenueCatUI.PaywallWebViewValue]? { - get - } - public var isNull: Swift.Bool { - get - } - public init(from decoder: any Swift.Decoder) throws - public func encode(to encoder: any Swift.Encoder) throws - public static func == (a: RevenueCatUI.PaywallWebViewValue, b: RevenueCatUI.PaywallWebViewValue) -> Swift.Bool - public func hash(into hasher: inout Swift.Hasher) - public var hashValue: Swift.Int { - get - } -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -@_Concurrency.MainActor public struct PaywallWebViewController { - @_Concurrency.MainActor public func postVariables(componentID: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) - @_Concurrency.MainActor public func postMessage(componentID: Swift.String, type: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewMessageAction { - public init(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) - @_Concurrency.MainActor public func callAsFunction(_ message: RevenueCatUI.PaywallWebViewMessage, _ controller: RevenueCatUI.PaywallWebViewController) -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -extension SwiftUICore.View { - @_Concurrency.MainActor @preconcurrency public func onPaywallWebViewMessage(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) -> some SwiftUICore.View - -} @_inheritsConvenienceInitializers @_hasMissingDesignatedInitializers @available(iOS 15.0, macOS 12.0, tvOS 15.0, *) @objc(RCPaywallFooterViewController) @_Concurrency.MainActor @preconcurrency final public class PaywallFooterViewController : RevenueCatUI.PaywallViewController { @_Concurrency.MainActor @preconcurrency public init(offering: RevenueCat.Offering? = nil, performPurchase: @escaping RevenueCatUI.PerformPurchase, performRestore: @escaping RevenueCatUI.PerformRestore, dismissRequestedHandler: ((_ controller: RevenueCatUI.PaywallViewController) -> Swift.Void)? = nil) @@ -606,7 +546,5 @@ extension RevenueCatUI.CustomerCenterNavigationLink : Swift.Sendable {} @available(iOS 15.0, macOS 12.0, watchOS 8.0, *) @available(tvOS, unavailable, message: "RevenueCatUI does not support tvOS yet") extension RevenueCatUI.PaywallView : Swift.Sendable {} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -extension RevenueCatUI.PaywallWebViewController : Swift.Sendable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Equatable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Hashable {} diff --git a/api/revenuecatui-api-ios.swiftinterface b/api/revenuecatui-api-ios.swiftinterface index 9514729189..5106fe465c 100644 --- a/api/revenuecatui-api-ios.swiftinterface +++ b/api/revenuecatui-api-ios.swiftinterface @@ -309,66 +309,6 @@ extension RevenueCat.Purchases { @available(iOS 15.0, macOS 12.0, *) @_Concurrency.MainActor @objc final public func presentPaywall(from url: Foundation.URL) -> Swift.Bool } -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewMessage : Swift.Sendable, Swift.Equatable { - public let componentID: Swift.String - public let type: Swift.String - public let responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? - public let error: Swift.String? - public init(componentID: Swift.String, type: Swift.String, responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? = nil, error: Swift.String? = nil) - public static func == (a: RevenueCatUI.PaywallWebViewMessage, b: RevenueCatUI.PaywallWebViewMessage) -> Swift.Bool -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewValue : Swift.Sendable, Swift.Equatable, Swift.Hashable, Swift.Codable { - public static func string(_ value: Swift.String) -> RevenueCatUI.PaywallWebViewValue - public static func number(_ value: Swift.Double) -> RevenueCatUI.PaywallWebViewValue - public static func bool(_ value: Swift.Bool) -> RevenueCatUI.PaywallWebViewValue - public static func array(_ value: [RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue - public static func object(_ value: [Swift.String : RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue - public static var null: RevenueCatUI.PaywallWebViewValue { - get - } - public var stringValue: Swift.String? { - get - } - public var numberValue: Swift.Double? { - get - } - public var boolValue: Swift.Bool? { - get - } - public var arrayValue: [RevenueCatUI.PaywallWebViewValue]? { - get - } - public var objectValue: [Swift.String : RevenueCatUI.PaywallWebViewValue]? { - get - } - public var isNull: Swift.Bool { - get - } - public init(from decoder: any Swift.Decoder) throws - public func encode(to encoder: any Swift.Encoder) throws - public static func == (a: RevenueCatUI.PaywallWebViewValue, b: RevenueCatUI.PaywallWebViewValue) -> Swift.Bool - public func hash(into hasher: inout Swift.Hasher) - public var hashValue: Swift.Int { - get - } -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -@_Concurrency.MainActor public struct PaywallWebViewController { - @_Concurrency.MainActor public func postVariables(componentID: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) - @_Concurrency.MainActor public func postMessage(componentID: Swift.String, type: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewMessageAction { - public init(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) - @_Concurrency.MainActor public func callAsFunction(_ message: RevenueCatUI.PaywallWebViewMessage, _ controller: RevenueCatUI.PaywallWebViewController) -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -extension SwiftUICore.View { - @_Concurrency.MainActor @preconcurrency public func onPaywallWebViewMessage(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) -> some SwiftUICore.View - -} @_inheritsConvenienceInitializers @_hasMissingDesignatedInitializers @available(iOS 15.0, macOS 12.0, tvOS 15.0, *) @objc(RCPaywallFooterViewController) @_Concurrency.MainActor @preconcurrency final public class PaywallFooterViewController : RevenueCatUI.PaywallViewController { @_Concurrency.MainActor @preconcurrency public init(offering: RevenueCat.Offering? = nil, performPurchase: @escaping RevenueCatUI.PerformPurchase, performRestore: @escaping RevenueCatUI.PerformRestore, dismissRequestedHandler: ((_ controller: RevenueCatUI.PaywallViewController) -> Swift.Void)? = nil) @@ -606,7 +546,5 @@ extension RevenueCatUI.CustomerCenterNavigationLink : Swift.Sendable {} @available(iOS 15.0, macOS 12.0, watchOS 8.0, *) @available(tvOS, unavailable, message: "RevenueCatUI does not support tvOS yet") extension RevenueCatUI.PaywallView : Swift.Sendable {} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -extension RevenueCatUI.PaywallWebViewController : Swift.Sendable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Equatable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Hashable {} diff --git a/api/revenuecatui-api-macos.swiftinterface b/api/revenuecatui-api-macos.swiftinterface index d7553c3e37..e64db71c52 100644 --- a/api/revenuecatui-api-macos.swiftinterface +++ b/api/revenuecatui-api-macos.swiftinterface @@ -168,66 +168,6 @@ open class CustomPaywallFontProvider : RevenueCatUI.PaywallFontProvider { @available(tvOS, unavailable, introduced: 15.0, message: "RevenueCatUI does not support tvOS yet") public typealias Body = @_opaqueReturnTypeOf("$s12RevenueCatUI11PaywallViewV4bodyQrvp", 0) __ } -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewMessage : Swift.Sendable, Swift.Equatable { - public let componentID: Swift.String - public let type: Swift.String - public let responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? - public let error: Swift.String? - public init(componentID: Swift.String, type: Swift.String, responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? = nil, error: Swift.String? = nil) - public static func == (a: RevenueCatUI.PaywallWebViewMessage, b: RevenueCatUI.PaywallWebViewMessage) -> Swift.Bool -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewValue : Swift.Sendable, Swift.Equatable, Swift.Hashable, Swift.Codable { - public static func string(_ value: Swift.String) -> RevenueCatUI.PaywallWebViewValue - public static func number(_ value: Swift.Double) -> RevenueCatUI.PaywallWebViewValue - public static func bool(_ value: Swift.Bool) -> RevenueCatUI.PaywallWebViewValue - public static func array(_ value: [RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue - public static func object(_ value: [Swift.String : RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue - public static var null: RevenueCatUI.PaywallWebViewValue { - get - } - public var stringValue: Swift.String? { - get - } - public var numberValue: Swift.Double? { - get - } - public var boolValue: Swift.Bool? { - get - } - public var arrayValue: [RevenueCatUI.PaywallWebViewValue]? { - get - } - public var objectValue: [Swift.String : RevenueCatUI.PaywallWebViewValue]? { - get - } - public var isNull: Swift.Bool { - get - } - public init(from decoder: any Swift.Decoder) throws - public func encode(to encoder: any Swift.Encoder) throws - public static func == (a: RevenueCatUI.PaywallWebViewValue, b: RevenueCatUI.PaywallWebViewValue) -> Swift.Bool - public func hash(into hasher: inout Swift.Hasher) - public var hashValue: Swift.Int { - get - } -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -@_Concurrency.MainActor public struct PaywallWebViewController { - @_Concurrency.MainActor public func postVariables(componentID: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) - @_Concurrency.MainActor public func postMessage(componentID: Swift.String, type: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewMessageAction { - public init(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) - @_Concurrency.MainActor public func callAsFunction(_ message: RevenueCatUI.PaywallWebViewMessage, _ controller: RevenueCatUI.PaywallWebViewController) -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -extension SwiftUICore.View { - @_Concurrency.MainActor @preconcurrency public func onPaywallWebViewMessage(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) -> some SwiftUICore.View - -} @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) extension SwiftUICore.View { @_Concurrency.MainActor @preconcurrency public func onWebPurchaseRedemptionAttempt(perform completion: @escaping @Sendable (RevenueCat.WebPurchaseRedemptionResult) -> Swift.Void) -> some SwiftUICore.View @@ -384,7 +324,5 @@ extension SwiftUICore.View { @available(iOS 15.0, macOS 12.0, watchOS 8.0, *) @available(tvOS, unavailable, message: "RevenueCatUI does not support tvOS yet") extension RevenueCatUI.PaywallView : Swift.Sendable {} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -extension RevenueCatUI.PaywallWebViewController : Swift.Sendable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Equatable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Hashable {} diff --git a/api/revenuecatui-api-visionos-simulator.swiftinterface b/api/revenuecatui-api-visionos-simulator.swiftinterface index 7369428ec5..b87d2b82d9 100644 --- a/api/revenuecatui-api-visionos-simulator.swiftinterface +++ b/api/revenuecatui-api-visionos-simulator.swiftinterface @@ -179,66 +179,6 @@ extension RevenueCat.Purchases { @available(iOS 15.0, macOS 12.0, *) @_Concurrency.MainActor @objc final public func presentPaywall(from url: Foundation.URL) -> Swift.Bool } -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewMessage : Swift.Sendable, Swift.Equatable { - public let componentID: Swift.String - public let type: Swift.String - public let responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? - public let error: Swift.String? - public init(componentID: Swift.String, type: Swift.String, responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? = nil, error: Swift.String? = nil) - public static func == (a: RevenueCatUI.PaywallWebViewMessage, b: RevenueCatUI.PaywallWebViewMessage) -> Swift.Bool -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewValue : Swift.Sendable, Swift.Equatable, Swift.Hashable, Swift.Codable { - public static func string(_ value: Swift.String) -> RevenueCatUI.PaywallWebViewValue - public static func number(_ value: Swift.Double) -> RevenueCatUI.PaywallWebViewValue - public static func bool(_ value: Swift.Bool) -> RevenueCatUI.PaywallWebViewValue - public static func array(_ value: [RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue - public static func object(_ value: [Swift.String : RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue - public static var null: RevenueCatUI.PaywallWebViewValue { - get - } - public var stringValue: Swift.String? { - get - } - public var numberValue: Swift.Double? { - get - } - public var boolValue: Swift.Bool? { - get - } - public var arrayValue: [RevenueCatUI.PaywallWebViewValue]? { - get - } - public var objectValue: [Swift.String : RevenueCatUI.PaywallWebViewValue]? { - get - } - public var isNull: Swift.Bool { - get - } - public init(from decoder: any Swift.Decoder) throws - public func encode(to encoder: any Swift.Encoder) throws - public static func == (a: RevenueCatUI.PaywallWebViewValue, b: RevenueCatUI.PaywallWebViewValue) -> Swift.Bool - public func hash(into hasher: inout Swift.Hasher) - public var hashValue: Swift.Int { - get - } -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -@_Concurrency.MainActor public struct PaywallWebViewController { - @_Concurrency.MainActor public func postVariables(componentID: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) - @_Concurrency.MainActor public func postMessage(componentID: Swift.String, type: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewMessageAction { - public init(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) - @_Concurrency.MainActor public func callAsFunction(_ message: RevenueCatUI.PaywallWebViewMessage, _ controller: RevenueCatUI.PaywallWebViewController) -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -extension SwiftUICore.View { - @_Concurrency.MainActor @preconcurrency public func onPaywallWebViewMessage(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) -> some SwiftUICore.View - -} @_inheritsConvenienceInitializers @_hasMissingDesignatedInitializers @available(iOS 15.0, macOS 12.0, tvOS 15.0, *) @objc(RCPaywallFooterViewController) @_Concurrency.MainActor @preconcurrency final public class PaywallFooterViewController : RevenueCatUI.PaywallViewController { @_Concurrency.MainActor @preconcurrency public init(offering: RevenueCat.Offering? = nil, performPurchase: @escaping RevenueCatUI.PerformPurchase, performRestore: @escaping RevenueCatUI.PerformRestore, dismissRequestedHandler: ((_ controller: RevenueCatUI.PaywallViewController) -> Swift.Void)? = nil) @@ -466,7 +406,5 @@ extension SwiftUICore.View { @available(iOS 15.0, macOS 12.0, watchOS 8.0, *) @available(tvOS, unavailable, message: "RevenueCatUI does not support tvOS yet") extension RevenueCatUI.PaywallView : Swift.Sendable {} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -extension RevenueCatUI.PaywallWebViewController : Swift.Sendable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Equatable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Hashable {} diff --git a/api/revenuecatui-api-visionos.swiftinterface b/api/revenuecatui-api-visionos.swiftinterface index cccba66c64..ec8d35ca1f 100644 --- a/api/revenuecatui-api-visionos.swiftinterface +++ b/api/revenuecatui-api-visionos.swiftinterface @@ -179,66 +179,6 @@ extension RevenueCat.Purchases { @available(iOS 15.0, macOS 12.0, *) @_Concurrency.MainActor @objc final public func presentPaywall(from url: Foundation.URL) -> Swift.Bool } -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewMessage : Swift.Sendable, Swift.Equatable { - public let componentID: Swift.String - public let type: Swift.String - public let responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? - public let error: Swift.String? - public init(componentID: Swift.String, type: Swift.String, responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? = nil, error: Swift.String? = nil) - public static func == (a: RevenueCatUI.PaywallWebViewMessage, b: RevenueCatUI.PaywallWebViewMessage) -> Swift.Bool -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewValue : Swift.Sendable, Swift.Equatable, Swift.Hashable, Swift.Codable { - public static func string(_ value: Swift.String) -> RevenueCatUI.PaywallWebViewValue - public static func number(_ value: Swift.Double) -> RevenueCatUI.PaywallWebViewValue - public static func bool(_ value: Swift.Bool) -> RevenueCatUI.PaywallWebViewValue - public static func array(_ value: [RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue - public static func object(_ value: [Swift.String : RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue - public static var null: RevenueCatUI.PaywallWebViewValue { - get - } - public var stringValue: Swift.String? { - get - } - public var numberValue: Swift.Double? { - get - } - public var boolValue: Swift.Bool? { - get - } - public var arrayValue: [RevenueCatUI.PaywallWebViewValue]? { - get - } - public var objectValue: [Swift.String : RevenueCatUI.PaywallWebViewValue]? { - get - } - public var isNull: Swift.Bool { - get - } - public init(from decoder: any Swift.Decoder) throws - public func encode(to encoder: any Swift.Encoder) throws - public static func == (a: RevenueCatUI.PaywallWebViewValue, b: RevenueCatUI.PaywallWebViewValue) -> Swift.Bool - public func hash(into hasher: inout Swift.Hasher) - public var hashValue: Swift.Int { - get - } -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -@_Concurrency.MainActor public struct PaywallWebViewController { - @_Concurrency.MainActor public func postVariables(componentID: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) - @_Concurrency.MainActor public func postMessage(componentID: Swift.String, type: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewMessageAction { - public init(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) - @_Concurrency.MainActor public func callAsFunction(_ message: RevenueCatUI.PaywallWebViewMessage, _ controller: RevenueCatUI.PaywallWebViewController) -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -extension SwiftUICore.View { - @_Concurrency.MainActor @preconcurrency public func onPaywallWebViewMessage(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) -> some SwiftUICore.View - -} @_inheritsConvenienceInitializers @_hasMissingDesignatedInitializers @available(iOS 15.0, macOS 12.0, tvOS 15.0, *) @objc(RCPaywallFooterViewController) @_Concurrency.MainActor @preconcurrency final public class PaywallFooterViewController : RevenueCatUI.PaywallViewController { @_Concurrency.MainActor @preconcurrency public init(offering: RevenueCat.Offering? = nil, performPurchase: @escaping RevenueCatUI.PerformPurchase, performRestore: @escaping RevenueCatUI.PerformRestore, dismissRequestedHandler: ((_ controller: RevenueCatUI.PaywallViewController) -> Swift.Void)? = nil) @@ -466,7 +406,5 @@ extension SwiftUICore.View { @available(iOS 15.0, macOS 12.0, watchOS 8.0, *) @available(tvOS, unavailable, message: "RevenueCatUI does not support tvOS yet") extension RevenueCatUI.PaywallView : Swift.Sendable {} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -extension RevenueCatUI.PaywallWebViewController : Swift.Sendable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Equatable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Hashable {} diff --git a/api/revenuecatui-api-watchos-simulator.swiftinterface b/api/revenuecatui-api-watchos-simulator.swiftinterface index 7038282c5d..03d128c761 100644 --- a/api/revenuecatui-api-watchos-simulator.swiftinterface +++ b/api/revenuecatui-api-watchos-simulator.swiftinterface @@ -166,66 +166,6 @@ open class CustomPaywallFontProvider : RevenueCatUI.PaywallFontProvider { @available(tvOS, unavailable, introduced: 15.0, message: "RevenueCatUI does not support tvOS yet") public typealias Body = @_opaqueReturnTypeOf("$s12RevenueCatUI11PaywallViewV4bodyQrvp", 0) __ } -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewMessage : Swift.Sendable, Swift.Equatable { - public let componentID: Swift.String - public let type: Swift.String - public let responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? - public let error: Swift.String? - public init(componentID: Swift.String, type: Swift.String, responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? = nil, error: Swift.String? = nil) - public static func == (a: RevenueCatUI.PaywallWebViewMessage, b: RevenueCatUI.PaywallWebViewMessage) -> Swift.Bool -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewValue : Swift.Sendable, Swift.Equatable, Swift.Hashable, Swift.Codable { - public static func string(_ value: Swift.String) -> RevenueCatUI.PaywallWebViewValue - public static func number(_ value: Swift.Double) -> RevenueCatUI.PaywallWebViewValue - public static func bool(_ value: Swift.Bool) -> RevenueCatUI.PaywallWebViewValue - public static func array(_ value: [RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue - public static func object(_ value: [Swift.String : RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue - public static var null: RevenueCatUI.PaywallWebViewValue { - get - } - public var stringValue: Swift.String? { - get - } - public var numberValue: Swift.Double? { - get - } - public var boolValue: Swift.Bool? { - get - } - public var arrayValue: [RevenueCatUI.PaywallWebViewValue]? { - get - } - public var objectValue: [Swift.String : RevenueCatUI.PaywallWebViewValue]? { - get - } - public var isNull: Swift.Bool { - get - } - public init(from decoder: any Swift.Decoder) throws - public func encode(to encoder: any Swift.Encoder) throws - public static func == (a: RevenueCatUI.PaywallWebViewValue, b: RevenueCatUI.PaywallWebViewValue) -> Swift.Bool - public func hash(into hasher: inout Swift.Hasher) - public var hashValue: Swift.Int { - get - } -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -@_Concurrency.MainActor public struct PaywallWebViewController { - @_Concurrency.MainActor public func postVariables(componentID: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) - @_Concurrency.MainActor public func postMessage(componentID: Swift.String, type: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewMessageAction { - public init(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) - @_Concurrency.MainActor public func callAsFunction(_ message: RevenueCatUI.PaywallWebViewMessage, _ controller: RevenueCatUI.PaywallWebViewController) -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -extension SwiftUICore.View { - @_Concurrency.MainActor @preconcurrency public func onPaywallWebViewMessage(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) -> some SwiftUICore.View - -} @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) extension SwiftUICore.View { @_Concurrency.MainActor @preconcurrency public func onWebPurchaseRedemptionAttempt(perform completion: @escaping @Sendable (RevenueCat.WebPurchaseRedemptionResult) -> Swift.Void) -> some SwiftUICore.View @@ -339,7 +279,5 @@ extension SwiftUICore.View { @available(iOS 15.0, macOS 12.0, watchOS 8.0, *) @available(tvOS, unavailable, message: "RevenueCatUI does not support tvOS yet") extension RevenueCatUI.PaywallView : Swift.Sendable {} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -extension RevenueCatUI.PaywallWebViewController : Swift.Sendable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Equatable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Hashable {} diff --git a/api/revenuecatui-api-watchos.swiftinterface b/api/revenuecatui-api-watchos.swiftinterface index 0490cf9e59..39b2ed3084 100644 --- a/api/revenuecatui-api-watchos.swiftinterface +++ b/api/revenuecatui-api-watchos.swiftinterface @@ -166,66 +166,6 @@ open class CustomPaywallFontProvider : RevenueCatUI.PaywallFontProvider { @available(tvOS, unavailable, introduced: 15.0, message: "RevenueCatUI does not support tvOS yet") public typealias Body = @_opaqueReturnTypeOf("$s12RevenueCatUI11PaywallViewV4bodyQrvp", 0) __ } -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewMessage : Swift.Sendable, Swift.Equatable { - public let componentID: Swift.String - public let type: Swift.String - public let responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? - public let error: Swift.String? - public init(componentID: Swift.String, type: Swift.String, responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? = nil, error: Swift.String? = nil) - public static func == (a: RevenueCatUI.PaywallWebViewMessage, b: RevenueCatUI.PaywallWebViewMessage) -> Swift.Bool -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewValue : Swift.Sendable, Swift.Equatable, Swift.Hashable, Swift.Codable { - public static func string(_ value: Swift.String) -> RevenueCatUI.PaywallWebViewValue - public static func number(_ value: Swift.Double) -> RevenueCatUI.PaywallWebViewValue - public static func bool(_ value: Swift.Bool) -> RevenueCatUI.PaywallWebViewValue - public static func array(_ value: [RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue - public static func object(_ value: [Swift.String : RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue - public static var null: RevenueCatUI.PaywallWebViewValue { - get - } - public var stringValue: Swift.String? { - get - } - public var numberValue: Swift.Double? { - get - } - public var boolValue: Swift.Bool? { - get - } - public var arrayValue: [RevenueCatUI.PaywallWebViewValue]? { - get - } - public var objectValue: [Swift.String : RevenueCatUI.PaywallWebViewValue]? { - get - } - public var isNull: Swift.Bool { - get - } - public init(from decoder: any Swift.Decoder) throws - public func encode(to encoder: any Swift.Encoder) throws - public static func == (a: RevenueCatUI.PaywallWebViewValue, b: RevenueCatUI.PaywallWebViewValue) -> Swift.Bool - public func hash(into hasher: inout Swift.Hasher) - public var hashValue: Swift.Int { - get - } -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -@_Concurrency.MainActor public struct PaywallWebViewController { - @_Concurrency.MainActor public func postVariables(componentID: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) - @_Concurrency.MainActor public func postMessage(componentID: Swift.String, type: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewMessageAction { - public init(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) - @_Concurrency.MainActor public func callAsFunction(_ message: RevenueCatUI.PaywallWebViewMessage, _ controller: RevenueCatUI.PaywallWebViewController) -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -extension SwiftUICore.View { - @_Concurrency.MainActor @preconcurrency public func onPaywallWebViewMessage(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) -> some SwiftUICore.View - -} @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) extension SwiftUICore.View { @_Concurrency.MainActor @preconcurrency public func onWebPurchaseRedemptionAttempt(perform completion: @escaping @Sendable (RevenueCat.WebPurchaseRedemptionResult) -> Swift.Void) -> some SwiftUICore.View @@ -339,7 +279,5 @@ extension SwiftUICore.View { @available(iOS 15.0, macOS 12.0, watchOS 8.0, *) @available(tvOS, unavailable, message: "RevenueCatUI does not support tvOS yet") extension RevenueCatUI.PaywallView : Swift.Sendable {} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -extension RevenueCatUI.PaywallWebViewController : Swift.Sendable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Equatable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Hashable {} From 41401ace9f3bd3224dd6d5f96526e658abe5ca0f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 16 Jul 2026 20:01:56 +0000 Subject: [PATCH 04/13] refactor(paywalls): keep web view bridge internal Co-authored-by: Jacob Rakidzich --- .../WebView/PaywallWebViewAPI.swift | 61 ++++++++------- .../AllAPITests.xcodeproj/project.pbxproj | 4 - .../PaywallWebViewAPI.swift | 77 ------------------- ...enuecatui-api-ios-simulator.swiftinterface | 62 --------------- api/revenuecatui-api-ios.swiftinterface | 62 --------------- api/revenuecatui-api-macos.swiftinterface | 62 --------------- ...atui-api-visionos-simulator.swiftinterface | 62 --------------- api/revenuecatui-api-visionos.swiftinterface | 62 --------------- ...catui-api-watchos-simulator.swiftinterface | 62 --------------- api/revenuecatui-api-watchos.swiftinterface | 62 --------------- 10 files changed, 30 insertions(+), 546 deletions(-) delete mode 100644 Tests/APITesters/AllAPITests/RevenueCatUISwiftAPITester/PaywallWebViewAPI.swift diff --git a/RevenueCatUI/Templates/V2/Components/WebView/PaywallWebViewAPI.swift b/RevenueCatUI/Templates/V2/Components/WebView/PaywallWebViewAPI.swift index 5ba2a3fe3b..4aed083814 100644 --- a/RevenueCatUI/Templates/V2/Components/WebView/PaywallWebViewAPI.swift +++ b/RevenueCatUI/Templates/V2/Components/WebView/PaywallWebViewAPI.swift @@ -1,26 +1,25 @@ import Foundation import SwiftUI -// swiftlint:disable missing_docs #if !os(tvOS) // For Paywalls V2 /// A validated message sent from a Paywalls V2 `web_view` component to your app. @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewMessage: Sendable, Equatable { +struct PaywallWebViewMessage: Sendable, Equatable { /// The identifier of the `web_view` component that produced this message. - public let componentID: String + let componentID: String /// The message type, e.g. `"rc:step-complete"`. - public let type: String + let type: String /// The responses collected by the web flow. Only populated for `"rc:step-complete"` messages. - public let responses: [String: PaywallWebViewValue]? + let responses: [String: PaywallWebViewValue]? /// The error reported by the web content. Only populated for `"rc:error"` messages. - public let error: String? + let error: String? - public init( + init( componentID: String, type: String, responses: [String: PaywallWebViewValue]? = nil, @@ -36,7 +35,7 @@ public struct PaywallWebViewMessage: Sendable, Equatable { /// A JSON-compatible value exchanged between a Paywalls V2 `web_view` component and your app. @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewValue: Sendable, Equatable, Hashable, Codable { +struct PaywallWebViewValue: Sendable, Equatable, Hashable, Codable { private indirect enum Storage: Sendable, Equatable, Hashable { case string(String) @@ -54,54 +53,54 @@ public struct PaywallWebViewValue: Sendable, Equatable, Hashable, Codable { } /// Creates a string value. - public static func string(_ value: String) -> Self { Self(.string(value)) } + static func string(_ value: String) -> Self { Self(.string(value)) } /// Creates a numeric value. Non-finite values normalize to ``null`` because JSON cannot encode them. - public static func number(_ value: Double) -> Self { value.isFinite ? Self(.number(value)) : .null } + static func number(_ value: Double) -> Self { value.isFinite ? Self(.number(value)) : .null } /// Creates a boolean value. - public static func bool(_ value: Bool) -> Self { Self(.bool(value)) } + static func bool(_ value: Bool) -> Self { Self(.bool(value)) } /// Creates an array value. - public static func array(_ value: [PaywallWebViewValue]) -> Self { Self(.array(value)) } + static func array(_ value: [PaywallWebViewValue]) -> Self { Self(.array(value)) } /// Creates an object (dictionary) value. - public static func object(_ value: [String: PaywallWebViewValue]) -> Self { Self(.object(value)) } + static func object(_ value: [String: PaywallWebViewValue]) -> Self { Self(.object(value)) } /// A null value. - public static var null: Self { Self(.null) } + static var null: Self { Self(.null) } - public var stringValue: String? { + var stringValue: String? { if case .string(let value) = self.storage { return value } return nil } - public var numberValue: Double? { + var numberValue: Double? { if case .number(let value) = self.storage { return value } return nil } - public var boolValue: Bool? { + var boolValue: Bool? { if case .bool(let value) = self.storage { return value } return nil } - public var arrayValue: [PaywallWebViewValue]? { + var arrayValue: [PaywallWebViewValue]? { if case .array(let value) = self.storage { return value } return nil } - public var objectValue: [String: PaywallWebViewValue]? { + var objectValue: [String: PaywallWebViewValue]? { if case .object(let value) = self.storage { return value } return nil } - public var isNull: Bool { + var isNull: Bool { if case .null = self.storage { return true } return false } - public init(from decoder: Decoder) throws { + init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() if container.decodeNil() { @@ -119,7 +118,7 @@ public struct PaywallWebViewValue: Sendable, Equatable, Hashable, Codable { } } - public func encode(to encoder: Encoder) throws { + func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() switch self.storage { @@ -142,34 +141,34 @@ public struct PaywallWebViewValue: Sendable, Equatable, Hashable, Codable { @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @MainActor -public struct PaywallWebViewController { +struct PaywallWebViewController { init() {} - public func postVariables(componentID: String, variables: [String: PaywallWebViewValue]) { - // Session wiring lands in a later PR; public API is inert until then. + func postVariables(componentID: String, variables: [String: PaywallWebViewValue]) { + // Session wiring lands in a later PR. } - public func postMessage(componentID: String, type: String, variables: [String: PaywallWebViewValue]) { - // Session wiring lands in a later PR; public API is inert until then. + func postMessage(componentID: String, type: String, variables: [String: PaywallWebViewValue]) { + // Session wiring lands in a later PR. } } /// A wrapper for the Paywalls V2 `web_view` message handler. @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewMessageAction { +struct PaywallWebViewMessageAction { private let action: @MainActor (PaywallWebViewMessage, PaywallWebViewController) -> Void - public init( + init( _ action: @escaping @MainActor (PaywallWebViewMessage, PaywallWebViewController) -> Void ) { self.action = action } @MainActor - public func callAsFunction(_ message: PaywallWebViewMessage, _ controller: PaywallWebViewController) { + func callAsFunction(_ message: PaywallWebViewMessage, _ controller: PaywallWebViewController) { self.action(message, controller) } @@ -194,7 +193,7 @@ extension EnvironmentValues { extension View { /// Invokes the given closure when a Paywalls V2 `web_view` component sends a message. - public func onPaywallWebViewMessage( + func onPaywallWebViewMessage( _ action: @escaping @MainActor (PaywallWebViewMessage, PaywallWebViewController) -> Void ) -> some View { self.environment(\.paywallWebViewMessageAction, PaywallWebViewMessageAction(action)) diff --git a/Tests/APITesters/AllAPITests/AllAPITests.xcodeproj/project.pbxproj b/Tests/APITesters/AllAPITests/AllAPITests.xcodeproj/project.pbxproj index 74a25fd257..9cc8116989 100644 --- a/Tests/APITesters/AllAPITests/AllAPITests.xcodeproj/project.pbxproj +++ b/Tests/APITesters/AllAPITests/AllAPITests.xcodeproj/project.pbxproj @@ -8,7 +8,6 @@ /* Begin PBXBuildFile section */ 1E8A60812CDCF29D0034ACF3 /* RedeemWebPurchasesAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E8A60802CDCF29D0034ACF3 /* RedeemWebPurchasesAPI.swift */; }; - B096F58208E0AA6582F34133 /* PaywallWebViewAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0DC08831A62751743D823FA /* PaywallWebViewAPI.swift */; }; 1EB697C22CD26218003000FC /* WebPurchaseRedemptionResultAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EB697C12CD26218003000FC /* WebPurchaseRedemptionResultAPI.swift */; }; 2D4C60E12C5AD1BC00A29FD2 /* AllAPITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2D4C60D82C5AD1BC00A29FD2 /* AllAPITests.framework */; }; 2D4C60E62C5AD1BC00A29FD2 /* AllAPITestsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D4C60E52C5AD1BC00A29FD2 /* AllAPITestsTests.swift */; }; @@ -229,7 +228,6 @@ /* Begin PBXFileReference section */ 1E8A60802CDCF29D0034ACF3 /* RedeemWebPurchasesAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedeemWebPurchasesAPI.swift; sourceTree = ""; }; - B0DC08831A62751743D823FA /* PaywallWebViewAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallWebViewAPI.swift; sourceTree = ""; }; 1EB697C12CD26218003000FC /* WebPurchaseRedemptionResultAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebPurchaseRedemptionResultAPI.swift; sourceTree = ""; }; 2D4C60D82C5AD1BC00A29FD2 /* AllAPITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AllAPITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 2D4C60DB2C5AD1BC00A29FD2 /* AllAPITests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AllAPITests.h; sourceTree = ""; }; @@ -647,7 +645,6 @@ children = ( FD33CD722D035876000D13A4 /* CustomerCenterViewControllerAPI.swift */, 2D4C622D2C5ADA6700A29FD2 /* PaywallViewAPI.swift */, - B0DC08831A62751743D823FA /* PaywallWebViewAPI.swift */, 1E8A60802CDCF29D0034ACF3 /* RedeemWebPurchasesAPI.swift */, 2D4C622B2C5ADA6700A29FD2 /* PaywallViewControllerAPI.swift */, 2D4C622C2C5ADA6700A29FD2 /* main.swift */, @@ -1122,7 +1119,6 @@ FDCC983A2D0A0FD500F5854B /* CustomerCenterViewAPI.swift in Sources */, 2D4C62302C5ADA6700A29FD2 /* PaywallViewControllerAPI.swift in Sources */, 1E8A60812CDCF29D0034ACF3 /* RedeemWebPurchasesAPI.swift in Sources */, - B096F58208E0AA6582F34133 /* PaywallWebViewAPI.swift in Sources */, 2D4C622E2C5ADA6700A29FD2 /* PaywallViewAPI.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Tests/APITesters/AllAPITests/RevenueCatUISwiftAPITester/PaywallWebViewAPI.swift b/Tests/APITesters/AllAPITests/RevenueCatUISwiftAPITester/PaywallWebViewAPI.swift deleted file mode 100644 index a8a2986ca9..0000000000 --- a/Tests/APITesters/AllAPITests/RevenueCatUISwiftAPITester/PaywallWebViewAPI.swift +++ /dev/null @@ -1,77 +0,0 @@ -// -// PaywallWebViewAPI.swift -// RevenueCatUISwiftAPITester -// -// Compile-time checks for the Paywalls V2 web_view public API surface. -// - -import RevenueCat -import RevenueCatUI -import SwiftUI - -#if !os(tvOS) // For Paywalls V2 - -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -struct PaywallWebViewApp: View { - - var body: some View { - Text("") - .onPaywallWebViewMessage { (message: PaywallWebViewMessage, controller: PaywallWebViewController) in - let componentID: String = message.componentID - let type: String = message.type - let responses: [String: PaywallWebViewValue]? = message.responses - let error: String? = message.error - - controller.postVariables( - componentID: componentID, - variables: ["custom": .object(["plan": .string("annual")])] - ) - controller.postMessage(componentID: componentID, type: type, variables: [:]) - - _ = (responses, error) - } - } - -} - -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -func checkPaywallWebViewMessageAPI() { - let message = PaywallWebViewMessage( - componentID: "", - type: "rc:step-complete", - responses: ["selected_plan": .string("annual")], - error: nil - ) - let sameMessage: Bool = message == message - _ = sameMessage -} - -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -func checkPaywallWebViewValueAPI() { - let string: PaywallWebViewValue = .string("s") - let number: PaywallWebViewValue = .number(1.5) - let bool: PaywallWebViewValue = .bool(true) - let array: PaywallWebViewValue = .array([string, number]) - let object: PaywallWebViewValue = .object(["key": bool]) - let null: PaywallWebViewValue = .null - - let _: String? = string.stringValue - let _: Double? = number.numberValue - let _: Bool? = bool.boolValue - let _: [PaywallWebViewValue]? = array.arrayValue - let _: [String: PaywallWebViewValue]? = object.objectValue - let _: Bool = null.isNull - let _: Set = [string, number] -} - -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -@MainActor -func checkPaywallWebViewMessageActionAPI( - message: PaywallWebViewMessage, - controller: PaywallWebViewController -) { - let action = PaywallWebViewMessageAction { (_: PaywallWebViewMessage, _: PaywallWebViewController) in } - action(message, controller) -} - -#endif diff --git a/api/revenuecatui-api-ios-simulator.swiftinterface b/api/revenuecatui-api-ios-simulator.swiftinterface index 428cfba3b0..30bbb7549c 100644 --- a/api/revenuecatui-api-ios-simulator.swiftinterface +++ b/api/revenuecatui-api-ios-simulator.swiftinterface @@ -309,66 +309,6 @@ extension RevenueCat.Purchases { @available(iOS 15.0, macOS 12.0, *) @_Concurrency.MainActor @objc final public func presentPaywall(from url: Foundation.URL) -> Swift.Bool } -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewMessage : Swift.Sendable, Swift.Equatable { - public let componentID: Swift.String - public let type: Swift.String - public let responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? - public let error: Swift.String? - public init(componentID: Swift.String, type: Swift.String, responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? = nil, error: Swift.String? = nil) - public static func == (a: RevenueCatUI.PaywallWebViewMessage, b: RevenueCatUI.PaywallWebViewMessage) -> Swift.Bool -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewValue : Swift.Sendable, Swift.Equatable, Swift.Hashable, Swift.Codable { - public static func string(_ value: Swift.String) -> RevenueCatUI.PaywallWebViewValue - public static func number(_ value: Swift.Double) -> RevenueCatUI.PaywallWebViewValue - public static func bool(_ value: Swift.Bool) -> RevenueCatUI.PaywallWebViewValue - public static func array(_ value: [RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue - public static func object(_ value: [Swift.String : RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue - public static var null: RevenueCatUI.PaywallWebViewValue { - get - } - public var stringValue: Swift.String? { - get - } - public var numberValue: Swift.Double? { - get - } - public var boolValue: Swift.Bool? { - get - } - public var arrayValue: [RevenueCatUI.PaywallWebViewValue]? { - get - } - public var objectValue: [Swift.String : RevenueCatUI.PaywallWebViewValue]? { - get - } - public var isNull: Swift.Bool { - get - } - public init(from decoder: any Swift.Decoder) throws - public func encode(to encoder: any Swift.Encoder) throws - public static func == (a: RevenueCatUI.PaywallWebViewValue, b: RevenueCatUI.PaywallWebViewValue) -> Swift.Bool - public func hash(into hasher: inout Swift.Hasher) - public var hashValue: Swift.Int { - get - } -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -@_Concurrency.MainActor public struct PaywallWebViewController { - @_Concurrency.MainActor public func postVariables(componentID: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) - @_Concurrency.MainActor public func postMessage(componentID: Swift.String, type: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewMessageAction { - public init(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) - @_Concurrency.MainActor public func callAsFunction(_ message: RevenueCatUI.PaywallWebViewMessage, _ controller: RevenueCatUI.PaywallWebViewController) -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -extension SwiftUICore.View { - @_Concurrency.MainActor @preconcurrency public func onPaywallWebViewMessage(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) -> some SwiftUICore.View - -} @_inheritsConvenienceInitializers @_hasMissingDesignatedInitializers @available(iOS 15.0, macOS 12.0, tvOS 15.0, *) @objc(RCPaywallFooterViewController) @_Concurrency.MainActor @preconcurrency final public class PaywallFooterViewController : RevenueCatUI.PaywallViewController { @_Concurrency.MainActor @preconcurrency public init(offering: RevenueCat.Offering? = nil, performPurchase: @escaping RevenueCatUI.PerformPurchase, performRestore: @escaping RevenueCatUI.PerformRestore, dismissRequestedHandler: ((_ controller: RevenueCatUI.PaywallViewController) -> Swift.Void)? = nil) @@ -606,7 +546,5 @@ extension RevenueCatUI.CustomerCenterNavigationLink : Swift.Sendable {} @available(iOS 15.0, macOS 12.0, watchOS 8.0, *) @available(tvOS, unavailable, message: "RevenueCatUI does not support tvOS yet") extension RevenueCatUI.PaywallView : Swift.Sendable {} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -extension RevenueCatUI.PaywallWebViewController : Swift.Sendable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Equatable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Hashable {} diff --git a/api/revenuecatui-api-ios.swiftinterface b/api/revenuecatui-api-ios.swiftinterface index 9514729189..5106fe465c 100644 --- a/api/revenuecatui-api-ios.swiftinterface +++ b/api/revenuecatui-api-ios.swiftinterface @@ -309,66 +309,6 @@ extension RevenueCat.Purchases { @available(iOS 15.0, macOS 12.0, *) @_Concurrency.MainActor @objc final public func presentPaywall(from url: Foundation.URL) -> Swift.Bool } -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewMessage : Swift.Sendable, Swift.Equatable { - public let componentID: Swift.String - public let type: Swift.String - public let responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? - public let error: Swift.String? - public init(componentID: Swift.String, type: Swift.String, responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? = nil, error: Swift.String? = nil) - public static func == (a: RevenueCatUI.PaywallWebViewMessage, b: RevenueCatUI.PaywallWebViewMessage) -> Swift.Bool -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewValue : Swift.Sendable, Swift.Equatable, Swift.Hashable, Swift.Codable { - public static func string(_ value: Swift.String) -> RevenueCatUI.PaywallWebViewValue - public static func number(_ value: Swift.Double) -> RevenueCatUI.PaywallWebViewValue - public static func bool(_ value: Swift.Bool) -> RevenueCatUI.PaywallWebViewValue - public static func array(_ value: [RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue - public static func object(_ value: [Swift.String : RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue - public static var null: RevenueCatUI.PaywallWebViewValue { - get - } - public var stringValue: Swift.String? { - get - } - public var numberValue: Swift.Double? { - get - } - public var boolValue: Swift.Bool? { - get - } - public var arrayValue: [RevenueCatUI.PaywallWebViewValue]? { - get - } - public var objectValue: [Swift.String : RevenueCatUI.PaywallWebViewValue]? { - get - } - public var isNull: Swift.Bool { - get - } - public init(from decoder: any Swift.Decoder) throws - public func encode(to encoder: any Swift.Encoder) throws - public static func == (a: RevenueCatUI.PaywallWebViewValue, b: RevenueCatUI.PaywallWebViewValue) -> Swift.Bool - public func hash(into hasher: inout Swift.Hasher) - public var hashValue: Swift.Int { - get - } -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -@_Concurrency.MainActor public struct PaywallWebViewController { - @_Concurrency.MainActor public func postVariables(componentID: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) - @_Concurrency.MainActor public func postMessage(componentID: Swift.String, type: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewMessageAction { - public init(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) - @_Concurrency.MainActor public func callAsFunction(_ message: RevenueCatUI.PaywallWebViewMessage, _ controller: RevenueCatUI.PaywallWebViewController) -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -extension SwiftUICore.View { - @_Concurrency.MainActor @preconcurrency public func onPaywallWebViewMessage(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) -> some SwiftUICore.View - -} @_inheritsConvenienceInitializers @_hasMissingDesignatedInitializers @available(iOS 15.0, macOS 12.0, tvOS 15.0, *) @objc(RCPaywallFooterViewController) @_Concurrency.MainActor @preconcurrency final public class PaywallFooterViewController : RevenueCatUI.PaywallViewController { @_Concurrency.MainActor @preconcurrency public init(offering: RevenueCat.Offering? = nil, performPurchase: @escaping RevenueCatUI.PerformPurchase, performRestore: @escaping RevenueCatUI.PerformRestore, dismissRequestedHandler: ((_ controller: RevenueCatUI.PaywallViewController) -> Swift.Void)? = nil) @@ -606,7 +546,5 @@ extension RevenueCatUI.CustomerCenterNavigationLink : Swift.Sendable {} @available(iOS 15.0, macOS 12.0, watchOS 8.0, *) @available(tvOS, unavailable, message: "RevenueCatUI does not support tvOS yet") extension RevenueCatUI.PaywallView : Swift.Sendable {} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -extension RevenueCatUI.PaywallWebViewController : Swift.Sendable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Equatable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Hashable {} diff --git a/api/revenuecatui-api-macos.swiftinterface b/api/revenuecatui-api-macos.swiftinterface index d7553c3e37..e64db71c52 100644 --- a/api/revenuecatui-api-macos.swiftinterface +++ b/api/revenuecatui-api-macos.swiftinterface @@ -168,66 +168,6 @@ open class CustomPaywallFontProvider : RevenueCatUI.PaywallFontProvider { @available(tvOS, unavailable, introduced: 15.0, message: "RevenueCatUI does not support tvOS yet") public typealias Body = @_opaqueReturnTypeOf("$s12RevenueCatUI11PaywallViewV4bodyQrvp", 0) __ } -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewMessage : Swift.Sendable, Swift.Equatable { - public let componentID: Swift.String - public let type: Swift.String - public let responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? - public let error: Swift.String? - public init(componentID: Swift.String, type: Swift.String, responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? = nil, error: Swift.String? = nil) - public static func == (a: RevenueCatUI.PaywallWebViewMessage, b: RevenueCatUI.PaywallWebViewMessage) -> Swift.Bool -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewValue : Swift.Sendable, Swift.Equatable, Swift.Hashable, Swift.Codable { - public static func string(_ value: Swift.String) -> RevenueCatUI.PaywallWebViewValue - public static func number(_ value: Swift.Double) -> RevenueCatUI.PaywallWebViewValue - public static func bool(_ value: Swift.Bool) -> RevenueCatUI.PaywallWebViewValue - public static func array(_ value: [RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue - public static func object(_ value: [Swift.String : RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue - public static var null: RevenueCatUI.PaywallWebViewValue { - get - } - public var stringValue: Swift.String? { - get - } - public var numberValue: Swift.Double? { - get - } - public var boolValue: Swift.Bool? { - get - } - public var arrayValue: [RevenueCatUI.PaywallWebViewValue]? { - get - } - public var objectValue: [Swift.String : RevenueCatUI.PaywallWebViewValue]? { - get - } - public var isNull: Swift.Bool { - get - } - public init(from decoder: any Swift.Decoder) throws - public func encode(to encoder: any Swift.Encoder) throws - public static func == (a: RevenueCatUI.PaywallWebViewValue, b: RevenueCatUI.PaywallWebViewValue) -> Swift.Bool - public func hash(into hasher: inout Swift.Hasher) - public var hashValue: Swift.Int { - get - } -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -@_Concurrency.MainActor public struct PaywallWebViewController { - @_Concurrency.MainActor public func postVariables(componentID: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) - @_Concurrency.MainActor public func postMessage(componentID: Swift.String, type: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewMessageAction { - public init(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) - @_Concurrency.MainActor public func callAsFunction(_ message: RevenueCatUI.PaywallWebViewMessage, _ controller: RevenueCatUI.PaywallWebViewController) -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -extension SwiftUICore.View { - @_Concurrency.MainActor @preconcurrency public func onPaywallWebViewMessage(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) -> some SwiftUICore.View - -} @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) extension SwiftUICore.View { @_Concurrency.MainActor @preconcurrency public func onWebPurchaseRedemptionAttempt(perform completion: @escaping @Sendable (RevenueCat.WebPurchaseRedemptionResult) -> Swift.Void) -> some SwiftUICore.View @@ -384,7 +324,5 @@ extension SwiftUICore.View { @available(iOS 15.0, macOS 12.0, watchOS 8.0, *) @available(tvOS, unavailable, message: "RevenueCatUI does not support tvOS yet") extension RevenueCatUI.PaywallView : Swift.Sendable {} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -extension RevenueCatUI.PaywallWebViewController : Swift.Sendable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Equatable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Hashable {} diff --git a/api/revenuecatui-api-visionos-simulator.swiftinterface b/api/revenuecatui-api-visionos-simulator.swiftinterface index 7369428ec5..b87d2b82d9 100644 --- a/api/revenuecatui-api-visionos-simulator.swiftinterface +++ b/api/revenuecatui-api-visionos-simulator.swiftinterface @@ -179,66 +179,6 @@ extension RevenueCat.Purchases { @available(iOS 15.0, macOS 12.0, *) @_Concurrency.MainActor @objc final public func presentPaywall(from url: Foundation.URL) -> Swift.Bool } -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewMessage : Swift.Sendable, Swift.Equatable { - public let componentID: Swift.String - public let type: Swift.String - public let responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? - public let error: Swift.String? - public init(componentID: Swift.String, type: Swift.String, responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? = nil, error: Swift.String? = nil) - public static func == (a: RevenueCatUI.PaywallWebViewMessage, b: RevenueCatUI.PaywallWebViewMessage) -> Swift.Bool -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewValue : Swift.Sendable, Swift.Equatable, Swift.Hashable, Swift.Codable { - public static func string(_ value: Swift.String) -> RevenueCatUI.PaywallWebViewValue - public static func number(_ value: Swift.Double) -> RevenueCatUI.PaywallWebViewValue - public static func bool(_ value: Swift.Bool) -> RevenueCatUI.PaywallWebViewValue - public static func array(_ value: [RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue - public static func object(_ value: [Swift.String : RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue - public static var null: RevenueCatUI.PaywallWebViewValue { - get - } - public var stringValue: Swift.String? { - get - } - public var numberValue: Swift.Double? { - get - } - public var boolValue: Swift.Bool? { - get - } - public var arrayValue: [RevenueCatUI.PaywallWebViewValue]? { - get - } - public var objectValue: [Swift.String : RevenueCatUI.PaywallWebViewValue]? { - get - } - public var isNull: Swift.Bool { - get - } - public init(from decoder: any Swift.Decoder) throws - public func encode(to encoder: any Swift.Encoder) throws - public static func == (a: RevenueCatUI.PaywallWebViewValue, b: RevenueCatUI.PaywallWebViewValue) -> Swift.Bool - public func hash(into hasher: inout Swift.Hasher) - public var hashValue: Swift.Int { - get - } -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -@_Concurrency.MainActor public struct PaywallWebViewController { - @_Concurrency.MainActor public func postVariables(componentID: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) - @_Concurrency.MainActor public func postMessage(componentID: Swift.String, type: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewMessageAction { - public init(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) - @_Concurrency.MainActor public func callAsFunction(_ message: RevenueCatUI.PaywallWebViewMessage, _ controller: RevenueCatUI.PaywallWebViewController) -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -extension SwiftUICore.View { - @_Concurrency.MainActor @preconcurrency public func onPaywallWebViewMessage(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) -> some SwiftUICore.View - -} @_inheritsConvenienceInitializers @_hasMissingDesignatedInitializers @available(iOS 15.0, macOS 12.0, tvOS 15.0, *) @objc(RCPaywallFooterViewController) @_Concurrency.MainActor @preconcurrency final public class PaywallFooterViewController : RevenueCatUI.PaywallViewController { @_Concurrency.MainActor @preconcurrency public init(offering: RevenueCat.Offering? = nil, performPurchase: @escaping RevenueCatUI.PerformPurchase, performRestore: @escaping RevenueCatUI.PerformRestore, dismissRequestedHandler: ((_ controller: RevenueCatUI.PaywallViewController) -> Swift.Void)? = nil) @@ -466,7 +406,5 @@ extension SwiftUICore.View { @available(iOS 15.0, macOS 12.0, watchOS 8.0, *) @available(tvOS, unavailable, message: "RevenueCatUI does not support tvOS yet") extension RevenueCatUI.PaywallView : Swift.Sendable {} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -extension RevenueCatUI.PaywallWebViewController : Swift.Sendable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Equatable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Hashable {} diff --git a/api/revenuecatui-api-visionos.swiftinterface b/api/revenuecatui-api-visionos.swiftinterface index cccba66c64..ec8d35ca1f 100644 --- a/api/revenuecatui-api-visionos.swiftinterface +++ b/api/revenuecatui-api-visionos.swiftinterface @@ -179,66 +179,6 @@ extension RevenueCat.Purchases { @available(iOS 15.0, macOS 12.0, *) @_Concurrency.MainActor @objc final public func presentPaywall(from url: Foundation.URL) -> Swift.Bool } -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewMessage : Swift.Sendable, Swift.Equatable { - public let componentID: Swift.String - public let type: Swift.String - public let responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? - public let error: Swift.String? - public init(componentID: Swift.String, type: Swift.String, responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? = nil, error: Swift.String? = nil) - public static func == (a: RevenueCatUI.PaywallWebViewMessage, b: RevenueCatUI.PaywallWebViewMessage) -> Swift.Bool -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewValue : Swift.Sendable, Swift.Equatable, Swift.Hashable, Swift.Codable { - public static func string(_ value: Swift.String) -> RevenueCatUI.PaywallWebViewValue - public static func number(_ value: Swift.Double) -> RevenueCatUI.PaywallWebViewValue - public static func bool(_ value: Swift.Bool) -> RevenueCatUI.PaywallWebViewValue - public static func array(_ value: [RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue - public static func object(_ value: [Swift.String : RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue - public static var null: RevenueCatUI.PaywallWebViewValue { - get - } - public var stringValue: Swift.String? { - get - } - public var numberValue: Swift.Double? { - get - } - public var boolValue: Swift.Bool? { - get - } - public var arrayValue: [RevenueCatUI.PaywallWebViewValue]? { - get - } - public var objectValue: [Swift.String : RevenueCatUI.PaywallWebViewValue]? { - get - } - public var isNull: Swift.Bool { - get - } - public init(from decoder: any Swift.Decoder) throws - public func encode(to encoder: any Swift.Encoder) throws - public static func == (a: RevenueCatUI.PaywallWebViewValue, b: RevenueCatUI.PaywallWebViewValue) -> Swift.Bool - public func hash(into hasher: inout Swift.Hasher) - public var hashValue: Swift.Int { - get - } -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -@_Concurrency.MainActor public struct PaywallWebViewController { - @_Concurrency.MainActor public func postVariables(componentID: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) - @_Concurrency.MainActor public func postMessage(componentID: Swift.String, type: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewMessageAction { - public init(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) - @_Concurrency.MainActor public func callAsFunction(_ message: RevenueCatUI.PaywallWebViewMessage, _ controller: RevenueCatUI.PaywallWebViewController) -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -extension SwiftUICore.View { - @_Concurrency.MainActor @preconcurrency public func onPaywallWebViewMessage(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) -> some SwiftUICore.View - -} @_inheritsConvenienceInitializers @_hasMissingDesignatedInitializers @available(iOS 15.0, macOS 12.0, tvOS 15.0, *) @objc(RCPaywallFooterViewController) @_Concurrency.MainActor @preconcurrency final public class PaywallFooterViewController : RevenueCatUI.PaywallViewController { @_Concurrency.MainActor @preconcurrency public init(offering: RevenueCat.Offering? = nil, performPurchase: @escaping RevenueCatUI.PerformPurchase, performRestore: @escaping RevenueCatUI.PerformRestore, dismissRequestedHandler: ((_ controller: RevenueCatUI.PaywallViewController) -> Swift.Void)? = nil) @@ -466,7 +406,5 @@ extension SwiftUICore.View { @available(iOS 15.0, macOS 12.0, watchOS 8.0, *) @available(tvOS, unavailable, message: "RevenueCatUI does not support tvOS yet") extension RevenueCatUI.PaywallView : Swift.Sendable {} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -extension RevenueCatUI.PaywallWebViewController : Swift.Sendable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Equatable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Hashable {} diff --git a/api/revenuecatui-api-watchos-simulator.swiftinterface b/api/revenuecatui-api-watchos-simulator.swiftinterface index 7038282c5d..03d128c761 100644 --- a/api/revenuecatui-api-watchos-simulator.swiftinterface +++ b/api/revenuecatui-api-watchos-simulator.swiftinterface @@ -166,66 +166,6 @@ open class CustomPaywallFontProvider : RevenueCatUI.PaywallFontProvider { @available(tvOS, unavailable, introduced: 15.0, message: "RevenueCatUI does not support tvOS yet") public typealias Body = @_opaqueReturnTypeOf("$s12RevenueCatUI11PaywallViewV4bodyQrvp", 0) __ } -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewMessage : Swift.Sendable, Swift.Equatable { - public let componentID: Swift.String - public let type: Swift.String - public let responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? - public let error: Swift.String? - public init(componentID: Swift.String, type: Swift.String, responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? = nil, error: Swift.String? = nil) - public static func == (a: RevenueCatUI.PaywallWebViewMessage, b: RevenueCatUI.PaywallWebViewMessage) -> Swift.Bool -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewValue : Swift.Sendable, Swift.Equatable, Swift.Hashable, Swift.Codable { - public static func string(_ value: Swift.String) -> RevenueCatUI.PaywallWebViewValue - public static func number(_ value: Swift.Double) -> RevenueCatUI.PaywallWebViewValue - public static func bool(_ value: Swift.Bool) -> RevenueCatUI.PaywallWebViewValue - public static func array(_ value: [RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue - public static func object(_ value: [Swift.String : RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue - public static var null: RevenueCatUI.PaywallWebViewValue { - get - } - public var stringValue: Swift.String? { - get - } - public var numberValue: Swift.Double? { - get - } - public var boolValue: Swift.Bool? { - get - } - public var arrayValue: [RevenueCatUI.PaywallWebViewValue]? { - get - } - public var objectValue: [Swift.String : RevenueCatUI.PaywallWebViewValue]? { - get - } - public var isNull: Swift.Bool { - get - } - public init(from decoder: any Swift.Decoder) throws - public func encode(to encoder: any Swift.Encoder) throws - public static func == (a: RevenueCatUI.PaywallWebViewValue, b: RevenueCatUI.PaywallWebViewValue) -> Swift.Bool - public func hash(into hasher: inout Swift.Hasher) - public var hashValue: Swift.Int { - get - } -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -@_Concurrency.MainActor public struct PaywallWebViewController { - @_Concurrency.MainActor public func postVariables(componentID: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) - @_Concurrency.MainActor public func postMessage(componentID: Swift.String, type: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewMessageAction { - public init(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) - @_Concurrency.MainActor public func callAsFunction(_ message: RevenueCatUI.PaywallWebViewMessage, _ controller: RevenueCatUI.PaywallWebViewController) -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -extension SwiftUICore.View { - @_Concurrency.MainActor @preconcurrency public func onPaywallWebViewMessage(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) -> some SwiftUICore.View - -} @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) extension SwiftUICore.View { @_Concurrency.MainActor @preconcurrency public func onWebPurchaseRedemptionAttempt(perform completion: @escaping @Sendable (RevenueCat.WebPurchaseRedemptionResult) -> Swift.Void) -> some SwiftUICore.View @@ -339,7 +279,5 @@ extension SwiftUICore.View { @available(iOS 15.0, macOS 12.0, watchOS 8.0, *) @available(tvOS, unavailable, message: "RevenueCatUI does not support tvOS yet") extension RevenueCatUI.PaywallView : Swift.Sendable {} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -extension RevenueCatUI.PaywallWebViewController : Swift.Sendable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Equatable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Hashable {} diff --git a/api/revenuecatui-api-watchos.swiftinterface b/api/revenuecatui-api-watchos.swiftinterface index 0490cf9e59..39b2ed3084 100644 --- a/api/revenuecatui-api-watchos.swiftinterface +++ b/api/revenuecatui-api-watchos.swiftinterface @@ -166,66 +166,6 @@ open class CustomPaywallFontProvider : RevenueCatUI.PaywallFontProvider { @available(tvOS, unavailable, introduced: 15.0, message: "RevenueCatUI does not support tvOS yet") public typealias Body = @_opaqueReturnTypeOf("$s12RevenueCatUI11PaywallViewV4bodyQrvp", 0) __ } -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewMessage : Swift.Sendable, Swift.Equatable { - public let componentID: Swift.String - public let type: Swift.String - public let responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? - public let error: Swift.String? - public init(componentID: Swift.String, type: Swift.String, responses: [Swift.String : RevenueCatUI.PaywallWebViewValue]? = nil, error: Swift.String? = nil) - public static func == (a: RevenueCatUI.PaywallWebViewMessage, b: RevenueCatUI.PaywallWebViewMessage) -> Swift.Bool -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewValue : Swift.Sendable, Swift.Equatable, Swift.Hashable, Swift.Codable { - public static func string(_ value: Swift.String) -> RevenueCatUI.PaywallWebViewValue - public static func number(_ value: Swift.Double) -> RevenueCatUI.PaywallWebViewValue - public static func bool(_ value: Swift.Bool) -> RevenueCatUI.PaywallWebViewValue - public static func array(_ value: [RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue - public static func object(_ value: [Swift.String : RevenueCatUI.PaywallWebViewValue]) -> RevenueCatUI.PaywallWebViewValue - public static var null: RevenueCatUI.PaywallWebViewValue { - get - } - public var stringValue: Swift.String? { - get - } - public var numberValue: Swift.Double? { - get - } - public var boolValue: Swift.Bool? { - get - } - public var arrayValue: [RevenueCatUI.PaywallWebViewValue]? { - get - } - public var objectValue: [Swift.String : RevenueCatUI.PaywallWebViewValue]? { - get - } - public var isNull: Swift.Bool { - get - } - public init(from decoder: any Swift.Decoder) throws - public func encode(to encoder: any Swift.Encoder) throws - public static func == (a: RevenueCatUI.PaywallWebViewValue, b: RevenueCatUI.PaywallWebViewValue) -> Swift.Bool - public func hash(into hasher: inout Swift.Hasher) - public var hashValue: Swift.Int { - get - } -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -@_Concurrency.MainActor public struct PaywallWebViewController { - @_Concurrency.MainActor public func postVariables(componentID: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) - @_Concurrency.MainActor public func postMessage(componentID: Swift.String, type: Swift.String, variables: [Swift.String : RevenueCatUI.PaywallWebViewValue]) -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct PaywallWebViewMessageAction { - public init(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) - @_Concurrency.MainActor public func callAsFunction(_ message: RevenueCatUI.PaywallWebViewMessage, _ controller: RevenueCatUI.PaywallWebViewController) -} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -extension SwiftUICore.View { - @_Concurrency.MainActor @preconcurrency public func onPaywallWebViewMessage(_ action: @escaping @_Concurrency.MainActor (RevenueCatUI.PaywallWebViewMessage, RevenueCatUI.PaywallWebViewController) -> Swift.Void) -> some SwiftUICore.View - -} @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) extension SwiftUICore.View { @_Concurrency.MainActor @preconcurrency public func onWebPurchaseRedemptionAttempt(perform completion: @escaping @Sendable (RevenueCat.WebPurchaseRedemptionResult) -> Swift.Void) -> some SwiftUICore.View @@ -339,7 +279,5 @@ extension SwiftUICore.View { @available(iOS 15.0, macOS 12.0, watchOS 8.0, *) @available(tvOS, unavailable, message: "RevenueCatUI does not support tvOS yet") extension RevenueCatUI.PaywallView : Swift.Sendable {} -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -extension RevenueCatUI.PaywallWebViewController : Swift.Sendable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Equatable {} extension RevenueCatUI.PaywallPresentationMode : Swift.Hashable {} From 94982da16f8286703e8b3b77a3e6cddf54e6a592 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 17 Jul 2026 14:09:44 +0000 Subject: [PATCH 05/13] fix(paywalls): remove client-side web view payload limits Co-authored-by: Jacob Rakidzich --- .../Components/WebView/WebViewEnvelope.swift | 55 +------------------ .../PaywallsV2/WebViewEnvelopeTests.swift | 33 +++-------- 2 files changed, 11 insertions(+), 77 deletions(-) diff --git a/RevenueCatUI/Templates/V2/Components/WebView/WebViewEnvelope.swift b/RevenueCatUI/Templates/V2/Components/WebView/WebViewEnvelope.swift index 7905029df5..b336c868b2 100644 --- a/RevenueCatUI/Templates/V2/Components/WebView/WebViewEnvelope.swift +++ b/RevenueCatUI/Templates/V2/Components/WebView/WebViewEnvelope.swift @@ -19,11 +19,6 @@ enum WebViewEnvelope { static let messageTypeResize = "resize" static let messageTypeFit = "fit" - static let maxInboundFrameBytes = 65_536 - static let maxJSONDepth = 16 - /// Structural depth budget for a whole inbound frame: the envelope object itself (1 level) - /// plus a payload tree of at most `maxJSONDepth` levels. - static let maxFrameDepth = maxJSONDepth + 1 static let maxResizePoints: CGFloat = 10_000 static let resizeThreshold: CGFloat = 1 static let fallbackFitHeight: CGFloat = 100 @@ -83,15 +78,13 @@ enum WebViewEnvelope { static func decode(rawMessage: Any) -> Envelope? { let data: Data if let string = rawMessage as? String { - guard string.utf8.count <= Self.maxInboundFrameBytes, - let stringData = string.data(using: .utf8) else { + guard let stringData = string.data(using: .utf8) else { return nil } data = stringData } else if let dictionary = rawMessage as? [String: Any] { guard JSONSerialization.isValidJSONObject(dictionary), - let serialized = try? JSONSerialization.data(withJSONObject: dictionary), - serialized.count <= Self.maxInboundFrameBytes else { + let serialized = try? JSONSerialization.data(withJSONObject: dictionary) else { return nil } data = serialized @@ -99,13 +92,6 @@ enum WebViewEnvelope { return nil } - // Enforce the nesting limit BEFORE decoding: `PaywallWebViewValue.init(from:)` recurses - // per nesting level, so a hostile deeply-nested frame (tens of thousands of levels fit in - // 64 KiB) could otherwise overflow the stack before any post-decode check runs. - guard !Self.exceedsMaxDepth(data) else { - return nil - } - guard let envelope = try? JSONDecoder().decode(Envelope.self, from: data), envelope.channel == Self.channel else { return nil @@ -114,43 +100,6 @@ enum WebViewEnvelope { return envelope } - /// Non-recursive scan of the raw JSON bytes, tracking `{`/`[` nesting outside string - /// literals (honoring `\` escapes). Returns `true` when the depth exceeds ``maxFrameDepth``. - static func exceedsMaxDepth(_ data: Data) -> Bool { - var depth = 0 - var inString = false - var escaped = false - - for byte in data { - if inString { - if escaped { - escaped = false - } else if byte == UInt8(ascii: "\\") { - escaped = true - } else if byte == UInt8(ascii: "\"") { - inString = false - } - continue - } - - switch byte { - case UInt8(ascii: "\""): - inString = true - case UInt8(ascii: "{"), UInt8(ascii: "["): - depth += 1 - if depth > Self.maxFrameDepth { - return true - } - case UInt8(ascii: "}"), UInt8(ascii: "]"): - depth -= 1 - default: - break - } - } - - return false - } - static func receiveScript(for envelope: Envelope) -> String? { let encoder = JSONEncoder() guard let data = try? encoder.encode(envelope), diff --git a/Tests/RevenueCatUITests/PaywallsV2/WebViewEnvelopeTests.swift b/Tests/RevenueCatUITests/PaywallsV2/WebViewEnvelopeTests.swift index 509b97964e..a5e12969eb 100644 --- a/Tests/RevenueCatUITests/PaywallsV2/WebViewEnvelopeTests.swift +++ b/Tests/RevenueCatUITests/PaywallsV2/WebViewEnvelopeTests.swift @@ -52,33 +52,18 @@ final class WebViewEnvelopeTests: TestCase { ])) } - func testByteLimit() { - let underLimitPayload = String(repeating: "x", count: WebViewEnvelope.maxInboundFrameBytes - 250) - XCTAssertNotNil(WebViewEnvelope.decode(rawMessage: Self.json([ - "channel": WebViewEnvelope.channel, - "protocol_version": 1, - "kind": "message", - "component_id": "web", - "type": "rc:step-loaded", - "payload": ["value": underLimitPayload] - ]))) + func testDoesNotEnforceClientSideByteLimit() { + let payload = String(repeating: "x", count: 65_537) + let frame = Self.frame(payload: ["value": payload]) - let overLimitPayload = String(repeating: "x", count: WebViewEnvelope.maxInboundFrameBytes + 1) - XCTAssertNil(WebViewEnvelope.decode(rawMessage: Self.json([ - "channel": WebViewEnvelope.channel, - "protocol_version": 1, - "kind": "message", - "component_id": "web", - "type": "rc:step-loaded", - "payload": ["value": overLimitPayload] - ]))) + XCTAssertNotNil(WebViewEnvelope.decode(rawMessage: frame)) + XCTAssertNotNil(WebViewEnvelope.decode(rawMessage: Self.json(frame))) } - func testDepthLimit() { - XCTAssertNotNil(WebViewEnvelope.decode( - rawMessage: Self.json(Self.frame(payload: Self.nestedObject(depth: 15))) - )) - XCTAssertNil(WebViewEnvelope.decode(rawMessage: Self.json(Self.frame(payload: Self.nestedObject(depth: 16))))) + func testDoesNotEnforceClientSideDepthLimit() { + XCTAssertNotNil( + WebViewEnvelope.decode(rawMessage: Self.json(Self.frame(payload: Self.nestedObject(depth: 16)))) + ) } func testEncodeDecodeRoundTripAndEscaping() throws { From ea6d24bf4933450bd663b6a937654703fcd54297 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 17 Jul 2026 14:20:57 +0000 Subject: [PATCH 06/13] test(paywalls): address web view envelope review Co-authored-by: Jacob Rakidzich --- .../Components/WebView/WebViewEnvelope.swift | 19 +++------- .../PaywallsV2/WebViewEnvelopeTests.swift | 37 +++++++++++++++---- 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/RevenueCatUI/Templates/V2/Components/WebView/WebViewEnvelope.swift b/RevenueCatUI/Templates/V2/Components/WebView/WebViewEnvelope.swift index b336c868b2..671aeb2fe9 100644 --- a/RevenueCatUI/Templates/V2/Components/WebView/WebViewEnvelope.swift +++ b/RevenueCatUI/Templates/V2/Components/WebView/WebViewEnvelope.swift @@ -1,5 +1,4 @@ import Foundation -// swiftlint:disable nesting #if !os(tvOS) // For Paywalls V2 @@ -24,6 +23,7 @@ enum WebViewEnvelope { static let fallbackFitHeight: CGFloat = 100 static let fallbackFitWidth: CGFloat = 300 + // swiftlint:disable nesting enum Kind: String, Codable { case connect case `init` @@ -74,21 +74,12 @@ enum WebViewEnvelope { case error } } + // swiftlint:enable nesting static func decode(rawMessage: Any) -> Envelope? { - let data: Data - if let string = rawMessage as? String { - guard let stringData = string.data(using: .utf8) else { - return nil - } - data = stringData - } else if let dictionary = rawMessage as? [String: Any] { - guard JSONSerialization.isValidJSONObject(dictionary), - let serialized = try? JSONSerialization.data(withJSONObject: dictionary) else { - return nil - } - data = serialized - } else { + // `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 } diff --git a/Tests/RevenueCatUITests/PaywallsV2/WebViewEnvelopeTests.swift b/Tests/RevenueCatUITests/PaywallsV2/WebViewEnvelopeTests.swift index a5e12969eb..58aa3bf426 100644 --- a/Tests/RevenueCatUITests/PaywallsV2/WebViewEnvelopeTests.swift +++ b/Tests/RevenueCatUITests/PaywallsV2/WebViewEnvelopeTests.swift @@ -43,20 +43,17 @@ final class WebViewEnvelopeTests: TestCase { "kind": "message" ]))) XCTAssertNil(WebViewEnvelope.decode(rawMessage: "{not json")) - XCTAssertNil(WebViewEnvelope.decode(rawMessage: [ - "channel": WebViewEnvelope.channel, - "protocol_version": 1, - "kind": "message", - "component_id": "web", - "payload": ["nested": ["bad": Date()]] - ])) + } + + func testRejectsNonStringFrames() { + XCTAssertNil(WebViewEnvelope.decode(rawMessage: Self.frame(payload: ["value": "valid JSON"]))) + XCTAssertNil(WebViewEnvelope.decode(rawMessage: 1)) } func testDoesNotEnforceClientSideByteLimit() { let payload = String(repeating: "x", count: 65_537) let frame = Self.frame(payload: ["value": payload]) - XCTAssertNotNil(WebViewEnvelope.decode(rawMessage: frame)) XCTAssertNotNil(WebViewEnvelope.decode(rawMessage: Self.json(frame))) } @@ -84,6 +81,30 @@ final class WebViewEnvelopeTests: TestCase { XCTAssertEqual(decoded.payload?["value"]?.stringValue, hostile) } + func testKindCodableRoundTrip() throws { + let kinds: [WebViewEnvelope.Kind] = [ + .connect, + .`init`, + .reject, + .message, + .request, + .response, + .error + ] + + for kind in kinds { + let data = try JSONEncoder().encode(kind) + XCTAssertEqual(try JSONDecoder().decode(WebViewEnvelope.Kind.self, from: data), kind) + } + } + + func testReservedPayloadKeysMatchWireProtocol() { + XCTAssertEqual( + WebViewEnvelope.reservedPayloadKeys, + ["channel", "protocol_version", "kind", "type", "component_id", "id", "error", "variables"] + ) + } + private static func frame(payload: [String: Any]) -> [String: Any] { [ "channel": WebViewEnvelope.channel, From 06125950309f465a64383feeb5b73fab4ba3703a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 17 Jul 2026 14:45:14 +0000 Subject: [PATCH 07/13] refactor(paywalls): remove unused web view handler API Co-authored-by: Jacob Rakidzich --- .../WebView/PaywallWebViewAPI.swift | 96 +------------------ 1 file changed, 2 insertions(+), 94 deletions(-) diff --git a/RevenueCatUI/Templates/V2/Components/WebView/PaywallWebViewAPI.swift b/RevenueCatUI/Templates/V2/Components/WebView/PaywallWebViewAPI.swift index 4aed083814..25e0d16a6c 100644 --- a/RevenueCatUI/Templates/V2/Components/WebView/PaywallWebViewAPI.swift +++ b/RevenueCatUI/Templates/V2/Components/WebView/PaywallWebViewAPI.swift @@ -1,44 +1,14 @@ import Foundation -import SwiftUI #if !os(tvOS) // For Paywalls V2 -/// A validated message sent from a Paywalls V2 `web_view` component to your app. -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -struct PaywallWebViewMessage: Sendable, Equatable { - - /// The identifier of the `web_view` component that produced this message. - let componentID: String - - /// The message type, e.g. `"rc:step-complete"`. - let type: String - - /// The responses collected by the web flow. Only populated for `"rc:step-complete"` messages. - let responses: [String: PaywallWebViewValue]? - - /// The error reported by the web content. Only populated for `"rc:error"` messages. - let error: String? - - init( - componentID: String, - type: String, - responses: [String: PaywallWebViewValue]? = nil, - error: String? = nil - ) { - self.componentID = componentID - self.type = type - self.responses = responses - self.error = error - } - -} - /// A JSON-compatible value exchanged between a Paywalls V2 `web_view` component and your app. @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) struct PaywallWebViewValue: Sendable, Equatable, Hashable, Codable { - private indirect enum Storage: Sendable, Equatable, Hashable { + private enum Storage: Sendable, Equatable, Hashable { case string(String) + // The web peer uses JavaScript `Number` values, so there is no integer precision to preserve. case number(Double) case bool(Bool) case array([PaywallWebViewValue]) @@ -139,66 +109,4 @@ struct PaywallWebViewValue: Sendable, Equatable, Hashable, Codable { } -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -@MainActor -struct PaywallWebViewController { - - init() {} - - func postVariables(componentID: String, variables: [String: PaywallWebViewValue]) { - // Session wiring lands in a later PR. - } - - func postMessage(componentID: String, type: String, variables: [String: PaywallWebViewValue]) { - // Session wiring lands in a later PR. - } - -} - -/// A wrapper for the Paywalls V2 `web_view` message handler. -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -struct PaywallWebViewMessageAction { - - private let action: @MainActor (PaywallWebViewMessage, PaywallWebViewController) -> Void - - init( - _ action: @escaping @MainActor (PaywallWebViewMessage, PaywallWebViewController) -> Void - ) { - self.action = action - } - - @MainActor - func callAsFunction(_ message: PaywallWebViewMessage, _ controller: PaywallWebViewController) { - self.action(message, controller) - } - -} - -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -struct PaywallWebViewMessageActionKey: EnvironmentKey { - static let defaultValue: PaywallWebViewMessageAction? = nil -} - -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -extension EnvironmentValues { - - var paywallWebViewMessageAction: PaywallWebViewMessageAction? { - get { self[PaywallWebViewMessageActionKey.self] } - set { self[PaywallWebViewMessageActionKey.self] = newValue } - } - -} - -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -extension View { - - /// Invokes the given closure when a Paywalls V2 `web_view` component sends a message. - func onPaywallWebViewMessage( - _ action: @escaping @MainActor (PaywallWebViewMessage, PaywallWebViewController) -> Void - ) -> some View { - self.environment(\.paywallWebViewMessageAction, PaywallWebViewMessageAction(action)) - } - -} - #endif From d14d63ca685f5932e3b031f700a11ccde72cef16 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 17 Jul 2026 14:46:22 +0000 Subject: [PATCH 08/13] fix(paywalls): retain internal bridge dependencies Co-authored-by: Jacob Rakidzich --- .../WebView/PaywallWebViewAPI.swift | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/RevenueCatUI/Templates/V2/Components/WebView/PaywallWebViewAPI.swift b/RevenueCatUI/Templates/V2/Components/WebView/PaywallWebViewAPI.swift index 25e0d16a6c..145b578a94 100644 --- a/RevenueCatUI/Templates/V2/Components/WebView/PaywallWebViewAPI.swift +++ b/RevenueCatUI/Templates/V2/Components/WebView/PaywallWebViewAPI.swift @@ -1,7 +1,38 @@ import Foundation +import SwiftUI #if !os(tvOS) // For Paywalls V2 +/// A validated message sent from a Paywalls V2 `web_view` component to your app. +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +struct PaywallWebViewMessage: Sendable, Equatable { + + /// The identifier of the `web_view` component that produced this message. + let componentID: String + + /// The message type, e.g. `"rc:step-complete"`. + let type: String + + /// The responses collected by the web flow. Only populated for `"rc:step-complete"` messages. + let responses: [String: PaywallWebViewValue]? + + /// The error reported by the web content. Only populated for `"rc:error"` messages. + let error: String? + + init( + componentID: String, + type: String, + responses: [String: PaywallWebViewValue]? = nil, + error: String? = nil + ) { + self.componentID = componentID + self.type = type + self.responses = responses + self.error = error + } + +} + /// A JSON-compatible value exchanged between a Paywalls V2 `web_view` component and your app. @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) struct PaywallWebViewValue: Sendable, Equatable, Hashable, Codable { @@ -109,4 +140,54 @@ struct PaywallWebViewValue: Sendable, Equatable, Hashable, Codable { } +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@MainActor +struct PaywallWebViewController { + + init() {} + + func postVariables(componentID: String, variables: [String: PaywallWebViewValue]) { + // Session wiring lands in a later PR. + } + + func postMessage(componentID: String, type: String, variables: [String: PaywallWebViewValue]) { + // Session wiring lands in a later PR. + } + +} + +/// A wrapper for the Paywalls V2 `web_view` message handler. +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +struct PaywallWebViewMessageAction { + + private let action: @MainActor (PaywallWebViewMessage, PaywallWebViewController) -> Void + + init( + _ action: @escaping @MainActor (PaywallWebViewMessage, PaywallWebViewController) -> Void + ) { + self.action = action + } + + @MainActor + func callAsFunction(_ message: PaywallWebViewMessage, _ controller: PaywallWebViewController) { + self.action(message, controller) + } + +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +struct PaywallWebViewMessageActionKey: EnvironmentKey { + static let defaultValue: PaywallWebViewMessageAction? = nil +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +extension EnvironmentValues { + + var paywallWebViewMessageAction: PaywallWebViewMessageAction? { + get { self[PaywallWebViewMessageActionKey.self] } + set { self[PaywallWebViewMessageActionKey.self] = newValue } + } + +} + #endif From 19cbfe67e290fc2c676da40fb99c58e53320ac1a Mon Sep 17 00:00:00 2001 From: Jacob Rakidzich Date: Fri, 17 Jul 2026 11:09:28 -0500 Subject: [PATCH 09/13] refactor(paywalls): remove web view message handler concept The bridge no longer forwards messages to an app-provided handler, so the message, action, controller, and environment types are removed. Only the JSON value type remains. Co-authored-by: Cursor --- .../WebView/PaywallWebViewAPI.swift | 81 ------------------- 1 file changed, 81 deletions(-) diff --git a/RevenueCatUI/Templates/V2/Components/WebView/PaywallWebViewAPI.swift b/RevenueCatUI/Templates/V2/Components/WebView/PaywallWebViewAPI.swift index 145b578a94..25e0d16a6c 100644 --- a/RevenueCatUI/Templates/V2/Components/WebView/PaywallWebViewAPI.swift +++ b/RevenueCatUI/Templates/V2/Components/WebView/PaywallWebViewAPI.swift @@ -1,38 +1,7 @@ import Foundation -import SwiftUI #if !os(tvOS) // For Paywalls V2 -/// A validated message sent from a Paywalls V2 `web_view` component to your app. -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -struct PaywallWebViewMessage: Sendable, Equatable { - - /// The identifier of the `web_view` component that produced this message. - let componentID: String - - /// The message type, e.g. `"rc:step-complete"`. - let type: String - - /// The responses collected by the web flow. Only populated for `"rc:step-complete"` messages. - let responses: [String: PaywallWebViewValue]? - - /// The error reported by the web content. Only populated for `"rc:error"` messages. - let error: String? - - init( - componentID: String, - type: String, - responses: [String: PaywallWebViewValue]? = nil, - error: String? = nil - ) { - self.componentID = componentID - self.type = type - self.responses = responses - self.error = error - } - -} - /// A JSON-compatible value exchanged between a Paywalls V2 `web_view` component and your app. @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) struct PaywallWebViewValue: Sendable, Equatable, Hashable, Codable { @@ -140,54 +109,4 @@ struct PaywallWebViewValue: Sendable, Equatable, Hashable, Codable { } -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -@MainActor -struct PaywallWebViewController { - - init() {} - - func postVariables(componentID: String, variables: [String: PaywallWebViewValue]) { - // Session wiring lands in a later PR. - } - - func postMessage(componentID: String, type: String, variables: [String: PaywallWebViewValue]) { - // Session wiring lands in a later PR. - } - -} - -/// A wrapper for the Paywalls V2 `web_view` message handler. -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -struct PaywallWebViewMessageAction { - - private let action: @MainActor (PaywallWebViewMessage, PaywallWebViewController) -> Void - - init( - _ action: @escaping @MainActor (PaywallWebViewMessage, PaywallWebViewController) -> Void - ) { - self.action = action - } - - @MainActor - func callAsFunction(_ message: PaywallWebViewMessage, _ controller: PaywallWebViewController) { - self.action(message, controller) - } - -} - -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -struct PaywallWebViewMessageActionKey: EnvironmentKey { - static let defaultValue: PaywallWebViewMessageAction? = nil -} - -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -extension EnvironmentValues { - - var paywallWebViewMessageAction: PaywallWebViewMessageAction? { - get { self[PaywallWebViewMessageActionKey.self] } - set { self[PaywallWebViewMessageActionKey.self] = newValue } - } - -} - #endif From 328ebf97c31aab8fa873123668385e2e3431efba Mon Sep 17 00:00:00 2001 From: Antonio Pallares Date: Mon, 20 Jul 2026 11:51:24 +0200 Subject: [PATCH 10/13] Rename PaywallWebViewAPI.swift to PaywallWebViewValue.swift, add header and tests Carries over the edits that didn't land in PR #1: file rename to match its sole `PaywallWebViewValue` type, copyright header, and added test coverage for `arrayValue`/nested accessors and top-level scalar/empty-container round-trips. Co-authored-by: Cursor --- RevenueCat.xcodeproj/project.pbxproj | 8 ++--- ...iewAPI.swift => PaywallWebViewValue.swift} | 4 +++ .../PaywallsV2/PaywallWebViewValueTests.swift | 30 +++++++++++++++++++ 3 files changed, 38 insertions(+), 4 deletions(-) rename RevenueCatUI/Templates/V2/Components/WebView/{PaywallWebViewAPI.swift => PaywallWebViewValue.swift} (98%) diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 1345105541..81b86a5ce2 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -109,7 +109,7 @@ 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 */; }; @@ -1673,7 +1673,7 @@ 16DA8ECC2E4EE33700283940 /* ImageComponentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageComponentTests.swift; sourceTree = ""; }; 16DA8EDE2E4F6A6600283940 /* PaywallVideoComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallVideoComponent.swift; sourceTree = ""; }; 16DA8EF32E4F7A2500283940 /* VideoComponentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoComponentTests.swift; sourceTree = ""; }; - 9D0C57ECDF0E7F1A5B4418FF /* PaywallWebViewAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallWebViewAPI.swift; sourceTree = ""; }; + 9D0C57ECDF0E7F1A5B4418FF /* PaywallWebViewValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallWebViewValue.swift; sourceTree = ""; }; 89DD761C9E00E61032AAEB44 /* WebViewEnvelope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewEnvelope.swift; sourceTree = ""; }; C1B939C7DAAFEA1DE2DE2674 /* WebViewEnvelopeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewEnvelopeTests.swift; sourceTree = ""; }; 686562C08D953A27986DB276 /* PaywallWebViewValueTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallWebViewValueTests.swift; sourceTree = ""; }; @@ -3376,7 +3376,7 @@ EB23E44596DD9714E81840CF /* WebView */ = { isa = PBXGroup; children = ( - 9D0C57ECDF0E7F1A5B4418FF /* PaywallWebViewAPI.swift */, + 9D0C57ECDF0E7F1A5B4418FF /* PaywallWebViewValue.swift */, 89DD761C9E00E61032AAEB44 /* WebViewEnvelope.swift */, ); path = WebView; @@ -8329,7 +8329,7 @@ 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 */, diff --git a/RevenueCatUI/Templates/V2/Components/WebView/PaywallWebViewAPI.swift b/RevenueCatUI/Templates/V2/Components/WebView/PaywallWebViewValue.swift similarity index 98% rename from RevenueCatUI/Templates/V2/Components/WebView/PaywallWebViewAPI.swift rename to RevenueCatUI/Templates/V2/Components/WebView/PaywallWebViewValue.swift index 25e0d16a6c..35610467d0 100644 --- a/RevenueCatUI/Templates/V2/Components/WebView/PaywallWebViewAPI.swift +++ b/RevenueCatUI/Templates/V2/Components/WebView/PaywallWebViewValue.swift @@ -1,3 +1,7 @@ +// +// Copyright RevenueCat Inc. All Rights Reserved. +// + import Foundation #if !os(tvOS) // For Paywalls V2 diff --git a/Tests/RevenueCatUITests/PaywallsV2/PaywallWebViewValueTests.swift b/Tests/RevenueCatUITests/PaywallsV2/PaywallWebViewValueTests.swift index a593eeb9c5..b06a7a176a 100644 --- a/Tests/RevenueCatUITests/PaywallsV2/PaywallWebViewValueTests.swift +++ b/Tests/RevenueCatUITests/PaywallsV2/PaywallWebViewValueTests.swift @@ -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 { From 4243461fad6eb0d94d5d4e343a1130540ee74e01 Mon Sep 17 00:00:00 2001 From: Antonio Pallares Date: Mon, 20 Jul 2026 13:10:36 +0200 Subject: [PATCH 11/13] Add envelope wire-key/rich-payload/rejection tests and brief Envelope doc Adds tests asserting the encoded frame uses snake_case wire keys and omits nil fields, decodes a full message frame with a rich payload, and rejects invalid protocol_version / non-string fields / non-object payload / non-object JSON. Co-authored-by: Cursor --- .../Components/WebView/WebViewEnvelope.swift | 1 + .../PaywallsV2/WebViewEnvelopeTests.swift | 109 ++++++++++++++++++ 2 files changed, 110 insertions(+) diff --git a/RevenueCatUI/Templates/V2/Components/WebView/WebViewEnvelope.swift b/RevenueCatUI/Templates/V2/Components/WebView/WebViewEnvelope.swift index 671aeb2fe9..98eacaaf4f 100644 --- a/RevenueCatUI/Templates/V2/Components/WebView/WebViewEnvelope.swift +++ b/RevenueCatUI/Templates/V2/Components/WebView/WebViewEnvelope.swift @@ -34,6 +34,7 @@ enum WebViewEnvelope { 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 diff --git a/Tests/RevenueCatUITests/PaywallsV2/WebViewEnvelopeTests.swift b/Tests/RevenueCatUITests/PaywallsV2/WebViewEnvelopeTests.swift index 58aa3bf426..29f9076490 100644 --- a/Tests/RevenueCatUITests/PaywallsV2/WebViewEnvelopeTests.swift +++ b/Tests/RevenueCatUITests/PaywallsV2/WebViewEnvelopeTests.swift @@ -63,6 +63,115 @@ final class WebViewEnvelopeTests: TestCase { ) } + func testEncodedFrameUsesSnakeCaseKeysAndOmitsNilFields() throws { + let envelope = WebViewEnvelope.Envelope(kind: .connect, componentID: "web") + + let data = try JSONEncoder().encode(envelope) + let object = try XCTUnwrap(try JSONSerialization.jsonObject(with: data) as? [String: Any]) + + XCTAssertEqual(Set(object.keys), ["channel", "protocol_version", "kind", "component_id"]) + XCTAssertEqual(object["channel"] as? String, WebViewEnvelope.channel) + XCTAssertEqual(object["protocol_version"] as? Int, 1) + XCTAssertEqual(object["kind"] as? String, "connect") + XCTAssertEqual(object["component_id"] as? String, "web") + } + + func testDecodesFullMessageFrameWithRichPayload() throws { + let envelope = try XCTUnwrap(WebViewEnvelope.decode(rawMessage: Self.json([ + "channel": WebViewEnvelope.channel, + "protocol_version": 1, + "kind": "response", + "component_id": "web", + "type": "rc:variables", + "id": "req-1", + "payload": [ + "string": "value", + "number": 1.25, + "bool": true, + "nested": ["inner": false], + "list": [1, 2, 3], + "nothing": NSNull() + ] + ]))) + + XCTAssertEqual(envelope.kind, .response) + XCTAssertEqual(envelope.componentID, "web") + XCTAssertEqual(envelope.type, "rc:variables") + XCTAssertEqual(envelope.id, "req-1") + + let payload = try XCTUnwrap(envelope.payload) + XCTAssertEqual(payload["string"]?.stringValue, "value") + XCTAssertEqual(payload["number"]?.numberValue, 1.25) + XCTAssertEqual(payload["bool"]?.boolValue, true) + XCTAssertEqual(payload["nested"]?.objectValue?["inner"]?.boolValue, false) + XCTAssertEqual(payload["list"]?.arrayValue?.count, 3) + XCTAssertTrue(payload["nothing"]?.isNull == true) + } + + func testRejectsInvalidProtocolVersion() { + // Missing protocol_version. + XCTAssertNil(WebViewEnvelope.decode(rawMessage: Self.json([ + "channel": WebViewEnvelope.channel, + "kind": "message", + "component_id": "web" + ]))) + // Fractional protocol_version (integral only). + XCTAssertNil(WebViewEnvelope.decode(rawMessage: Self.json([ + "channel": WebViewEnvelope.channel, + "protocol_version": 1.5, + "kind": "message", + "component_id": "web" + ]))) + // Non-numeric protocol_version. + XCTAssertNil(WebViewEnvelope.decode(rawMessage: Self.json([ + "channel": WebViewEnvelope.channel, + "protocol_version": "1", + "kind": "message", + "component_id": "web" + ]))) + } + + func testRejectsNonStringFieldsAndNonObjectPayload() { + // Non-string type. + XCTAssertNil(WebViewEnvelope.decode(rawMessage: Self.json([ + "channel": WebViewEnvelope.channel, + "protocol_version": 1, + "kind": "message", + "component_id": "web", + "type": 123 + ]))) + // Non-string id. + XCTAssertNil(WebViewEnvelope.decode(rawMessage: Self.json([ + "channel": WebViewEnvelope.channel, + "protocol_version": 1, + "kind": "response", + "component_id": "web", + "id": 123 + ]))) + // Non-string error. + XCTAssertNil(WebViewEnvelope.decode(rawMessage: Self.json([ + "channel": WebViewEnvelope.channel, + "protocol_version": 1, + "kind": "error", + "component_id": "web", + "error": 123 + ]))) + // Non-object payload. + XCTAssertNil(WebViewEnvelope.decode(rawMessage: Self.json([ + "channel": WebViewEnvelope.channel, + "protocol_version": 1, + "kind": "message", + "component_id": "web", + "payload": [1, 2, 3] + ]))) + } + + func testRejectsValidJSONThatIsNotAnObject() { + XCTAssertNil(WebViewEnvelope.decode(rawMessage: "[1,2,3]")) + XCTAssertNil(WebViewEnvelope.decode(rawMessage: "\"hello\"")) + XCTAssertNil(WebViewEnvelope.decode(rawMessage: "5")) + } + func testEncodeDecodeRoundTripAndEscaping() throws { let hostile = "annual\" }); alert('xss'); //\n\\ end\u{2028}\u{2029}" let envelope = WebViewEnvelope.Envelope( From 306e38d578fb741c4aeaeb0d45a9afa97f670a07 Mon Sep 17 00:00:00 2001 From: Antonio Pallares Date: Mon, 20 Jul 2026 14:20:30 +0200 Subject: [PATCH 12/13] Drop reservedPayloadKeys from envelope and simplify test helper Defer the reserved-key set to Part 5 where its only consumer lives, and make the test JSON helper throwing to remove the file-wide force_try disable. Co-authored-by: Cursor --- .../Components/WebView/WebViewEnvelope.swift | 4 -- .../PaywallsV2/WebViewEnvelopeTests.swift | 52 ++++++++----------- 2 files changed, 22 insertions(+), 34 deletions(-) diff --git a/RevenueCatUI/Templates/V2/Components/WebView/WebViewEnvelope.swift b/RevenueCatUI/Templates/V2/Components/WebView/WebViewEnvelope.swift index 98eacaaf4f..8e35ca5fe6 100644 --- a/RevenueCatUI/Templates/V2/Components/WebView/WebViewEnvelope.swift +++ b/RevenueCatUI/Templates/V2/Components/WebView/WebViewEnvelope.swift @@ -108,10 +108,6 @@ enum WebViewEnvelope { """ } - static let reservedPayloadKeys: Set = [ - "channel", "protocol_version", "kind", "type", "component_id", "id", "error", "variables" - ] - } #endif diff --git a/Tests/RevenueCatUITests/PaywallsV2/WebViewEnvelopeTests.swift b/Tests/RevenueCatUITests/PaywallsV2/WebViewEnvelopeTests.swift index 29f9076490..c16df99342 100644 --- a/Tests/RevenueCatUITests/PaywallsV2/WebViewEnvelopeTests.swift +++ b/Tests/RevenueCatUITests/PaywallsV2/WebViewEnvelopeTests.swift @@ -4,7 +4,6 @@ @testable import RevenueCatUI import XCTest -// swiftlint:disable force_try #if !os(tvOS) @@ -12,7 +11,7 @@ import XCTest final class WebViewEnvelopeTests: TestCase { func testDecodesValidConnectFrame() throws { - let envelope = WebViewEnvelope.decode(rawMessage: Self.json([ + let envelope = WebViewEnvelope.decode(rawMessage: try Self.json([ "channel": WebViewEnvelope.channel, "protocol_version": 1, "kind": "connect", @@ -24,20 +23,20 @@ final class WebViewEnvelopeTests: TestCase { XCTAssertEqual(envelope?.componentID, "") } - func testRejectsMalformedFrames() { - XCTAssertNil(WebViewEnvelope.decode(rawMessage: Self.json([ + func testRejectsMalformedFrames() throws { + XCTAssertNil(WebViewEnvelope.decode(rawMessage: try Self.json([ "channel": "wrong", "protocol_version": 1, "kind": "connect", "component_id": "" ]))) - XCTAssertNil(WebViewEnvelope.decode(rawMessage: Self.json([ + XCTAssertNil(WebViewEnvelope.decode(rawMessage: try Self.json([ "channel": WebViewEnvelope.channel, "protocol_version": 1, "kind": "unknown", "component_id": "" ]))) - XCTAssertNil(WebViewEnvelope.decode(rawMessage: Self.json([ + XCTAssertNil(WebViewEnvelope.decode(rawMessage: try Self.json([ "channel": WebViewEnvelope.channel, "protocol_version": 1, "kind": "message" @@ -50,16 +49,16 @@ final class WebViewEnvelopeTests: TestCase { XCTAssertNil(WebViewEnvelope.decode(rawMessage: 1)) } - func testDoesNotEnforceClientSideByteLimit() { + func testDoesNotEnforceClientSideByteLimit() throws { let payload = String(repeating: "x", count: 65_537) let frame = Self.frame(payload: ["value": payload]) - XCTAssertNotNil(WebViewEnvelope.decode(rawMessage: Self.json(frame))) + XCTAssertNotNil(WebViewEnvelope.decode(rawMessage: try Self.json(frame))) } - func testDoesNotEnforceClientSideDepthLimit() { + func testDoesNotEnforceClientSideDepthLimit() throws { XCTAssertNotNil( - WebViewEnvelope.decode(rawMessage: Self.json(Self.frame(payload: Self.nestedObject(depth: 16)))) + WebViewEnvelope.decode(rawMessage: try Self.json(Self.frame(payload: Self.nestedObject(depth: 16)))) ) } @@ -77,7 +76,7 @@ final class WebViewEnvelopeTests: TestCase { } func testDecodesFullMessageFrameWithRichPayload() throws { - let envelope = try XCTUnwrap(WebViewEnvelope.decode(rawMessage: Self.json([ + let envelope = try XCTUnwrap(WebViewEnvelope.decode(rawMessage: try Self.json([ "channel": WebViewEnvelope.channel, "protocol_version": 1, "kind": "response", @@ -108,22 +107,22 @@ final class WebViewEnvelopeTests: TestCase { XCTAssertTrue(payload["nothing"]?.isNull == true) } - func testRejectsInvalidProtocolVersion() { + func testRejectsInvalidProtocolVersion() throws { // Missing protocol_version. - XCTAssertNil(WebViewEnvelope.decode(rawMessage: Self.json([ + XCTAssertNil(WebViewEnvelope.decode(rawMessage: try Self.json([ "channel": WebViewEnvelope.channel, "kind": "message", "component_id": "web" ]))) // Fractional protocol_version (integral only). - XCTAssertNil(WebViewEnvelope.decode(rawMessage: Self.json([ + XCTAssertNil(WebViewEnvelope.decode(rawMessage: try Self.json([ "channel": WebViewEnvelope.channel, "protocol_version": 1.5, "kind": "message", "component_id": "web" ]))) // Non-numeric protocol_version. - XCTAssertNil(WebViewEnvelope.decode(rawMessage: Self.json([ + XCTAssertNil(WebViewEnvelope.decode(rawMessage: try Self.json([ "channel": WebViewEnvelope.channel, "protocol_version": "1", "kind": "message", @@ -131,9 +130,9 @@ final class WebViewEnvelopeTests: TestCase { ]))) } - func testRejectsNonStringFieldsAndNonObjectPayload() { + func testRejectsNonStringFieldsAndNonObjectPayload() throws { // Non-string type. - XCTAssertNil(WebViewEnvelope.decode(rawMessage: Self.json([ + XCTAssertNil(WebViewEnvelope.decode(rawMessage: try Self.json([ "channel": WebViewEnvelope.channel, "protocol_version": 1, "kind": "message", @@ -141,7 +140,7 @@ final class WebViewEnvelopeTests: TestCase { "type": 123 ]))) // Non-string id. - XCTAssertNil(WebViewEnvelope.decode(rawMessage: Self.json([ + XCTAssertNil(WebViewEnvelope.decode(rawMessage: try Self.json([ "channel": WebViewEnvelope.channel, "protocol_version": 1, "kind": "response", @@ -149,7 +148,7 @@ final class WebViewEnvelopeTests: TestCase { "id": 123 ]))) // Non-string error. - XCTAssertNil(WebViewEnvelope.decode(rawMessage: Self.json([ + XCTAssertNil(WebViewEnvelope.decode(rawMessage: try Self.json([ "channel": WebViewEnvelope.channel, "protocol_version": 1, "kind": "error", @@ -157,7 +156,7 @@ final class WebViewEnvelopeTests: TestCase { "error": 123 ]))) // Non-object payload. - XCTAssertNil(WebViewEnvelope.decode(rawMessage: Self.json([ + XCTAssertNil(WebViewEnvelope.decode(rawMessage: try Self.json([ "channel": WebViewEnvelope.channel, "protocol_version": 1, "kind": "message", @@ -207,13 +206,6 @@ final class WebViewEnvelopeTests: TestCase { } } - func testReservedPayloadKeysMatchWireProtocol() { - XCTAssertEqual( - WebViewEnvelope.reservedPayloadKeys, - ["channel", "protocol_version", "kind", "type", "component_id", "id", "error", "variables"] - ) - } - private static func frame(payload: [String: Any]) -> [String: Any] { [ "channel": WebViewEnvelope.channel, @@ -233,9 +225,9 @@ final class WebViewEnvelopeTests: TestCase { return value } - static func json(_ object: [String: Any]) -> String { - let data = try! JSONSerialization.data(withJSONObject: object) - return String(data: data, encoding: .utf8)! + static func json(_ object: [String: Any]) throws -> String { + let data = try JSONSerialization.data(withJSONObject: object) + return try XCTUnwrap(String(data: data, encoding: .utf8)) } static func decodeEnvelope(fromScript script: String) throws -> WebViewEnvelope.Envelope { From b70147002d276a53e8db8faf15e22807a408983e Mon Sep 17 00:00:00 2001 From: Antonio Pallares Date: Mon, 20 Jul 2026 20:09:07 +0200 Subject: [PATCH 13/13] Trim unused message types and move receiveScript out of the data model Keep only the `resize`/`fit` message types (matching Android) and drop the `rc:*` leftovers from a previous API design. Remove `receiveScript`/ `receiveFunction` from the envelope data model; they're bridge concerns and will live in WebViewSession in Part 5 where the sole consumer exists. Co-authored-by: Cursor --- .../Components/WebView/WebViewEnvelope.swift | 22 ------------------- .../PaywallsV2/WebViewEnvelopeTests.swift | 16 ++++---------- 2 files changed, 4 insertions(+), 34 deletions(-) diff --git a/RevenueCatUI/Templates/V2/Components/WebView/WebViewEnvelope.swift b/RevenueCatUI/Templates/V2/Components/WebView/WebViewEnvelope.swift index 8e35ca5fe6..58d972b9ee 100644 --- a/RevenueCatUI/Templates/V2/Components/WebView/WebViewEnvelope.swift +++ b/RevenueCatUI/Templates/V2/Components/WebView/WebViewEnvelope.swift @@ -7,14 +7,8 @@ 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" static let messageTypeResize = "resize" static let messageTypeFit = "fit" @@ -92,22 +86,6 @@ enum WebViewEnvelope { return envelope } - static func receiveScript(for envelope: Envelope) -> String? { - 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 diff --git a/Tests/RevenueCatUITests/PaywallsV2/WebViewEnvelopeTests.swift b/Tests/RevenueCatUITests/PaywallsV2/WebViewEnvelopeTests.swift index c16df99342..819e72f924 100644 --- a/Tests/RevenueCatUITests/PaywallsV2/WebViewEnvelopeTests.swift +++ b/Tests/RevenueCatUITests/PaywallsV2/WebViewEnvelopeTests.swift @@ -171,7 +171,7 @@ final class WebViewEnvelopeTests: TestCase { XCTAssertNil(WebViewEnvelope.decode(rawMessage: "5")) } - func testEncodeDecodeRoundTripAndEscaping() throws { + func testEncodeDecodeRoundTrip() throws { let hostile = "annual\" }); alert('xss'); //\n\\ end\u{2028}\u{2029}" let envelope = WebViewEnvelope.Envelope( kind: .message, @@ -180,11 +180,10 @@ final class WebViewEnvelopeTests: TestCase { payload: ["value": .string(hostile)] ) - let script = try XCTUnwrap(WebViewEnvelope.receiveScript(for: envelope)) - XCTAssertFalse(script.contains("\u{2028}")) - XCTAssertFalse(script.contains("\u{2029}")) + let data = try JSONEncoder().encode(envelope) + let json = try XCTUnwrap(String(data: data, encoding: .utf8)) - let decoded = try Self.decodeEnvelope(fromScript: script) + let decoded = try XCTUnwrap(WebViewEnvelope.decode(rawMessage: json)) XCTAssertEqual(decoded, envelope) XCTAssertEqual(decoded.payload?["value"]?.stringValue, hostile) } @@ -230,13 +229,6 @@ final class WebViewEnvelopeTests: TestCase { return try XCTUnwrap(String(data: data, encoding: .utf8)) } - static func decodeEnvelope(fromScript script: String) throws -> WebViewEnvelope.Envelope { - let start = try XCTUnwrap(script.range(of: "var m=")?.upperBound) - let end = try XCTUnwrap(script.range(of: ";if", range: start..