feat(paywalls): web_view bridge session with document-reset lifecycle (5/7)#7231
Conversation
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 <Jacob@JacobZivanDesign.com>
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 <Jacob@JacobZivanDesign.com>
…3/7) Extract origin normalization, main-frame navigation policy, and WK content rule isolation as inert helpers used by later session/view PRs. Co-authored-by: Jacob Rakidzich <Jacob@JacobZivanDesign.com>
Co-authored-by: Jacob Rakidzich <Jacob@JacobZivanDesign.com>
… (5/7) Add WebViewSession (using WebViewOrigin), restore controller session wiring, and document-reset so reloads can re-handshake. Includes session tests plus reconnect/resize-threshold/onDocumentReset coverage. Co-authored-by: Jacob Rakidzich <Jacob@JacobZivanDesign.com>
4 builds increased size
RevenueCat 1.0 (1)
|
| Item | Install Size Change |
|---|---|
| 📝 RevenueCatUI.WebViewSession.Objc Metadata | ⬆️ 18.9 kB |
| DYLD.String Table | ⬆️ 18.5 kB |
| 🗑 RCPurchases.logOutWithCompletion: | ⬇️ -13.3 kB |
| RCPurchases.Objc Metadata | ⬆️ 13.3 kB |
| 📝 RevenueCatUI.WebViewSession.WebViewSession | ⬆️ 2.9 kB |
BinarySizeTest 1.0 (1)
com.revenuecat.binary-size-test.local-source
⚖️ Compare build
📦 Install build
⏱️ Analyze build performance
Total install size change: ⬆️ 44.8 kB (0.34%)
Total download size change: ⬆️ 18.3 kB (0.41%)
Largest size changes
| Item | Install Size Change |
|---|---|
| DYLD.String Table | ⬆️ 2.9 kB |
| 📝 RevenueCatUI.WebViewEnvelope.Envelope.init(from) | ⬆️ 1.7 kB |
| RevenueCatUI.TemplateViewConfiguration.filter(packages,with) | ⬇️ -1.5 kB |
| DYLD.Lazy Bind | ⬆️ 1.5 kB |
| RevenueCatUI.TemplateViewConfiguration.PackageConfiguration.__der... | ⬆️ 1.4 kB |
BinarySizeTest 1.0 (1)
com.revenuecat.binary-size-test.cocoapods
⚖️ Compare build
📦 Install build
⏱️ Analyze build performance
Total install size change: ⬆️ 82.3 kB (0.28%)
Total download size change: ⬆️ 27.5 kB (0.41%)
Largest size changes
| Item | Install Size Change |
|---|---|
| DYLD.String Table | ⬆️ 26.0 kB |
| Code Signature | ⬆️ 2.4 kB |
| 📝 RevenueCatUI.WebViewEnvelope.Envelope.init(from) | ⬆️ 1.8 kB |
| RevenueCatUI.TemplateViewConfiguration.filter(packages,with) | ⬇️ -1.5 kB |
| DYLD.Lazy Bind | ⬆️ 1.5 kB |
BinarySizeTest 1.0 (1)
com.revenuecat.binary-size-test.spm
⚖️ Compare build
📦 Install build
⏱️ Analyze build performance
Total install size change: ⬆️ 34.9 kB (0.3%)
Total download size change: ⬆️ 18.5 kB (0.4%)
Largest size changes
| Item | Install Size Change |
|---|---|
| 📝 RevenueCatUI.WebViewEnvelope.Envelope.init(from) | ⬆️ 1.7 kB |
| RevenueCatUI.TemplateViewConfiguration.filter(packages,with) | ⬇️ -1.5 kB |
| RevenueCatUI.TemplateViewConfiguration.PackageConfiguration.__der... | ⬆️ 1.4 kB |
| 📝 RevenueCatUI.WebViewEnvelope.Envelope.__derived_struct_equals | ⬆️ 1.4 kB |
| 📝 RevenueCatUI.WebViewSession.send(allowBeforeNavigation) | ⬆️ 1.3 kB |
🛸 Powered by Emerge Tools
Co-authored-by: Jacob Rakidzich <Jacob@JacobZivanDesign.com>
Co-authored-by: Jacob Rakidzich <Jacob@JacobZivanDesign.com>
Co-authored-by: Jacob Rakidzich <Jacob@JacobZivanDesign.com>
Co-authored-by: Jacob Rakidzich <Jacob@JacobZivanDesign.com>
Co-authored-by: Jacob Rakidzich <Jacob@JacobZivanDesign.com>
Co-authored-by: Jacob Rakidzich <Jacob@JacobZivanDesign.com>
# Conflicts: # RevenueCatUI/Templates/V2/Components/WebView/PaywallWebViewAPI.swift Co-authored-by: Jacob Rakidzich <Jacob@JacobZivanDesign.com>
Co-authored-by: Jacob Rakidzich <Jacob@JacobZivanDesign.com>
Removes the message-handler concept from the session: no app-facing delivery, controller, or app-provided variable posting. The wire bridge (handshake, resize, variables auto-reply, origin gating) is unchanged. Co-authored-by: Cursor <cursoragent@cursor.com>
| return | ||
| } | ||
| } else if let payload = envelope.payload { | ||
| guard WebViewEnvelope.reservedPayloadKeys.isDisjoint(with: payload.keys) else { |
There was a problem hiding this comment.
I'm not sure we really need reservedPayloadKeys at all. I removed it from PR#2 (#7228) in 306e38d and we'll revisit here in this PR.
My initial intuition tells me that it shouldn't a problem to have the payload use the same key name as the Envelope itself, as they'd be used at different levels of the JSON anyway. But we'll see
Match Part 2: drop the unused `rc:*` message types (step-loaded, step-complete, request-variables, variables, error) and their bridge handlers, keeping only `resize`/`fit`. This also removes the now-dead SDK-variables/locale plumbing and `reservedPayloadKeys`. Move `receiveScript` (and its `receiveFunction` constant) from the envelope data model into WebViewSession, where the sole consumer lives, with tests. Co-authored-by: Cursor <cursoragent@cursor.com>
Retarget Part 5 onto main now that Parts 1-3 (#7227, #7228, #7229) are merged. Resolve conflicts by keeping main's canonical Part 1-3 files (PaywallWebViewValue, WebViewEnvelope, WebViewOriginPolicy and their tests) and dropping stale deps-123 artifacts (PaywallWebViewAPI.swift, WebViewIsolation.swift and its test, plus the now-unused content_rules_failed log string). Part 5's own additions (WebViewSession + tests, log strings) are preserved. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit f984e23. Configure here.
Validate inbound frames against the sender frame's securityOrigin instead of the WebView's top-level URL (parity with Android's sourceOrigin), keeping the top-level URL only as an outbound defense-in-depth check. Normalize expectedOrigin in init, drop the dead protocolVersion parameter in favor of the fixed supported version, move receiveFunction onto WebViewEnvelope, and log dropped frames at warning level. Adds origin-gating tests. Co-authored-by: Cursor <cursoragent@cursor.com>
|
…tensions Replace WebViewOrigin.origin(of:) static helpers with a webViewOrigin computed property on URL and WKSecurityOrigin, sharing a single private canonicalization helper. Also drop the redundant nonisolated/assumeIsolated on the script message handler since WebViewSession is already @mainactor. Co-authored-by: Cursor <cursoragent@cursor.com>
…m guard Co-authored-by: Cursor <cursoragent@cursor.com>
…port order Co-authored-by: Cursor <cursoragent@cursor.com>
| static let channel = "rc-web-components" | ||
| static let messageHandlerName = "rcWebComponents" | ||
| /// Name of the JS function injected into the web view that receives host-to-content frames. | ||
| static let receiveFunction = "__rcWebComponentsReceive" |
There was a problem hiding this comment.
Moved it here to make it equal to Android (+ it makes more sense to me)
There was a problem hiding this comment.
This file only contains a simple refactor to have getter webViewOrigin variables on URL and WKSecurityOrigin instead of two methods in WebViewOrigin. The WebViewOrigin enum is now a private namespace for the common canonicalOrigin method
Normalize expectedOrigin to an optional canonical origin; when it cannot be resolved, keep it nil and log a warning instead of silently trusting the raw string, so the bridge rejects all inbound/outbound traffic and the misconfiguration is diagnosable. Mirrors purchases-android. Also splits the outbound skip log into distinct reasons (channel-not-open vs untrusted-current-url), documents why handshake replies allow sending before the top-level URL settles, and fixes a stale render-only test comment. Co-authored-by: Cursor <cursoragent@cursor.com>
…onnect Pass only the protocol version into handleConnect (matching handleResize), annotate the JS line-terminator escaping, and trim comment wording. Co-authored-by: Cursor <cursoragent@cursor.com>
…inbound-drop assertions Add coverage for component-id mismatch, unknown message type, missing type, and the wrong-channel decode filter. Rework the existing inbound-drop tests to assert against an onContentResize sink with a real payload instead of captured outbound scripts, so a resize that should be dropped is distinguishable from a no-op and the tests fail if their gate is removed. Co-authored-by: Cursor <cursoragent@cursor.com>
|
@RCGitBot please test |





Checklist
purchases-androidand hybridsMotivation
Part 5 of 7 splitting #7183 (PWENG-99); inert until Part 7. Lands the security-sensitive WKWebView message bridge, retargeted onto
mainnow that Parts 1–3 (#7227, #7228, #7229) are merged.Description
WebViewSession, theWKScriptMessageHandlerhandshake bridge (resize/fit auto-reply) plus a document-reset lifecycle (resetForNewDocument/onDocumentReset) so a committed main-frame navigation can re-handshake instead of staying stuck behindchannelOpen.securityOrigin(parity with Android'ssourceOriginin MakePaywallViewControlleropen #3798); the WebView's top-level URL is only an outbound defense-in-depth check.Remaining stack: #7230 (4/7), #7232 (6/7), #7233 (7/7).