Skip to content

feat(paywalls): add web_view JavaScript bridge (PWENG-98)#3655

Draft
alexrepty wants to merge 5 commits into
alexrepty/paywalls-web-view-variables-providerfrom
alexrepty/paywalls-web-view-bridge
Draft

feat(paywalls): add web_view JavaScript bridge (PWENG-98)#3655
alexrepty wants to merge 5 commits into
alexrepty/paywalls-web-view-variables-providerfrom
alexrepty/paywalls-web-view-bridge

Conversation

@alexrepty

@alexrepty alexrepty commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Checklist

  • If applicable, unit tests
  • If applicable, create follow-up issues for purchases-ios and hybrids

Motivation

Provides the live JS↔native bridge that powers web_view bidirectional messaging, built on the message model and variables provider.

Part of PWENG-98.

Description

  • Adds WebViewJavaScriptBridge: installs a document-start window.RevenueCatWebView shim, validates and dispatches inbound messages (origin-checked, on the main thread), and delivers native→web messages via window.__revenueCatReceiveMessage.
  • Outgoing messages use the { "type", "component_id", "variables" } envelope (payload nested under variables), matching the other RevenueCat SDK platforms. On rc:request-variables the SDK auto-replies with its system variables, then invokes the app handler.
  • Internal; exercised by unit tests in isolation. Wiring into the rendered paywall lands in the final PR.
  • Tested by WebViewJavaScriptBridgeTest.

iOS parity: mirrors purchases-ios (web-view-bridge-wiring), including the { type, component_id, variables } envelope.
Stack (merge bottom-up): schema → rendering → CSP → value types → message model → variables provider → JS bridge → wiring.


Note

High Risk
Introduces a JavaScript interface on WebView with origin validation and CSP; mistakes could affect paywall security or hosted web flows, though the change includes explicit origin checks and extensive tests.

Overview
Adds bidirectional JS↔native messaging for Paywalls V2 web_view, aligned with workflow-web-components-sdk (rc-web-components channel, connect/init handshake, message/request/response frames) via window.rcWebComponents and __rcWebComponentsReceive—replacing a flat postMessage shape and document-start shim.

WebViewJavaScriptBridge handles handshake, origin checks, payload limits, auto-reply to rc:request-variables, fit/resize for fit sizing, and lifecycle on the main thread. WebViewMessageParser and WebViewEnvelope parse the new wire format; schema gains optional fallback stack.

WebViewComponentView installs the bridge when componentId is set, detects main-frame load failures (HTTP ≥400), renders the fallback stack, and adjusts layout from content-reported CSS size. Style factory passes componentId and fallback into WebViewComponentStyle; ComponentView forwards click/interaction callbacks to the web view path.

Coverage: WebViewJavaScriptBridgeTest and updated WebViewMessageParserTest.

Reviewed by Cursor Bugbot for commit 8063d3f. Bugbot is set up for automated code reviews on this repo. Configure here.

@alexrepty alexrepty changed the title feat(paywalls): add web_view bidirectional messaging feat(paywalls): add web_view JavaScript bridge engine Jun 25, 2026
@alexrepty alexrepty mentioned this pull request Jun 25, 2026
2 tasks
@alexrepty alexrepty changed the base branch from alexrepty/paywalls-web-view-message-model to graphite-base/3655 June 26, 2026 10:49
@alexrepty alexrepty force-pushed the alexrepty/paywalls-web-view-bridge branch from e9af9f9 to 1704224 Compare June 26, 2026 10:49
@alexrepty alexrepty force-pushed the graphite-base/3655 branch from 21899a1 to 276280e Compare June 26, 2026 10:49
@alexrepty alexrepty changed the base branch from graphite-base/3655 to alexrepty/paywalls-web-view-variables-provider June 26, 2026 10:50
@alexrepty alexrepty changed the title feat(paywalls): add web_view JavaScript bridge engine feat(paywalls): add web_view JavaScript bridge Jun 26, 2026
@alexrepty alexrepty force-pushed the alexrepty/paywalls-web-view-bridge branch from 1704224 to e63218e Compare June 26, 2026 14:20
@alexrepty alexrepty force-pushed the alexrepty/paywalls-web-view-variables-provider branch 2 times, most recently from 7769f56 to 716657f Compare June 26, 2026 14:26
@alexrepty alexrepty force-pushed the alexrepty/paywalls-web-view-bridge branch from e63218e to aab1540 Compare June 26, 2026 14:26
@alexrepty alexrepty force-pushed the alexrepty/paywalls-web-view-variables-provider branch from 716657f to 27bcaa4 Compare June 26, 2026 14:57
@alexrepty alexrepty force-pushed the alexrepty/paywalls-web-view-bridge branch from aab1540 to 8483042 Compare June 26, 2026 14:57
@alexrepty alexrepty force-pushed the alexrepty/paywalls-web-view-variables-provider branch from 27bcaa4 to c009f2d Compare June 29, 2026 09:30
@alexrepty alexrepty force-pushed the alexrepty/paywalls-web-view-bridge branch 2 times, most recently from 7b32cdc to b712772 Compare June 29, 2026 09:58
@alexrepty alexrepty force-pushed the alexrepty/paywalls-web-view-variables-provider branch from c009f2d to 3ca2b9e Compare June 29, 2026 09:58
@alexrepty alexrepty changed the title feat(paywalls): add web_view JavaScript bridge feat(paywalls): add web_view JavaScript bridge (PWENG-98) Jun 29, 2026
@alexrepty alexrepty force-pushed the alexrepty/paywalls-web-view-bridge branch from b712772 to 9627fbf Compare June 29, 2026 12:53
@alexrepty alexrepty force-pushed the alexrepty/paywalls-web-view-variables-provider branch 2 times, most recently from 9c03cae to c73e87b Compare June 29, 2026 13:58
@alexrepty alexrepty force-pushed the alexrepty/paywalls-web-view-bridge branch from 9627fbf to 5fcb286 Compare June 29, 2026 13:58
@alexrepty alexrepty force-pushed the alexrepty/paywalls-web-view-variables-provider branch from c73e87b to cf8f802 Compare June 29, 2026 14:14
@alexrepty alexrepty force-pushed the alexrepty/paywalls-web-view-bridge branch from 5fcb286 to 6b3546c Compare June 29, 2026 14:14
@alexrepty alexrepty marked this pull request as ready for review June 29, 2026 14:43
@alexrepty alexrepty requested a review from a team as a code owner June 29, 2026 14:43
@alexrepty alexrepty force-pushed the alexrepty/paywalls-web-view-bridge branch 2 times, most recently from b7e4c63 to 35d1a2c Compare June 30, 2026 07:44
@alexrepty alexrepty force-pushed the alexrepty/paywalls-web-view-variables-provider branch from cf8f802 to f341b57 Compare June 30, 2026 07:44
@alexrepty alexrepty force-pushed the alexrepty/paywalls-web-view-variables-provider branch from f341b57 to 4fcc3b7 Compare June 30, 2026 08:43
@alexrepty alexrepty force-pushed the alexrepty/paywalls-web-view-bridge branch from 35d1a2c to 39fe849 Compare June 30, 2026 08:43
@AlvaroBrey AlvaroBrey force-pushed the alexrepty/paywalls-web-view-variables-provider branch from 4fcc3b7 to 7298c01 Compare July 6, 2026 08:12
@AlvaroBrey AlvaroBrey force-pushed the alexrepty/paywalls-web-view-bridge branch from 39fe849 to 28307d9 Compare July 6, 2026 08:12
@AlvaroBrey AlvaroBrey self-assigned this Jul 6, 2026
Adds a bidirectional JS<->native bridge for the Paywalls V2 `web_view` component: a document-start `window.RevenueCatWebView` shim, message parsing/validation (`WebViewMessageParser`, `WebViewMessageType`), the public `PaywallWebViewMessage` / `PaywallWebViewValue` / `PaywallWebViewMessageHandler` / `PaywallWebViewController` surface, and an SDK-managed variables provider (locale, color scheme, custom variables). Apps opt in via `PaywallOptions.setWebViewMessageHandler(...)`, threaded through `PaywallState` / `OfferingToStateMapper`.

Regenerates ui/revenuecatui/api.txt for the new public API.

Recommended labels: pr:feat, pr:RevenueCatUI, feat:Paywalls_V2
@AlvaroBrey AlvaroBrey force-pushed the alexrepty/paywalls-web-view-variables-provider branch from 7298c01 to e94849b Compare July 6, 2026 13:57
@AlvaroBrey AlvaroBrey force-pushed the alexrepty/paywalls-web-view-bridge branch from 28307d9 to 26368d7 Compare July 6, 2026 13:57
// origin: an explicit default port and an omitted one must not cause spurious message drops.
val effectivePort = if (port == -1) defaultPort else port
val portSuffix = if (effectivePort == -1) "" else ":$effectivePort"
return "$protocol://$host$portSuffix"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Case-sensitive origin host matching

Medium Severity

toOriginOrNull builds an origin string with the host exactly as java.net.URL returns it, and isCurrentOriginExpected compares that to an origin parsed from webView.url using plain string equality. Hostnames are case-insensitive per URL rules, and Android’s WebView often lowercases the host in getUrl() after load. A paywall URL whose template keeps mixed-case ASCII in the host can make every inbound and outbound bridge message look like a cross-origin mismatch and be dropped.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 26368d7. Configure here.

@emerge-tools

emerge-tools Bot commented Jul 6, 2026

Copy link
Copy Markdown

📸 Snapshot Test

593 unchanged

Name Added Removed Modified Renamed Unchanged Errored Approval
TestPurchasesUIAndroidCompatibility Paparazzi
com.revenuecat.testpurchasesuiandroidcompatibility.paparazzi
0 0 0 0 258 0 N/A
TestPurchasesUIAndroidCompatibility
com.revenuecat.testpurchasesuiandroidcompatibility
0 0 0 0 335 0 N/A

🛸 Powered by Emerge Tools

@AlvaroBrey AlvaroBrey marked this pull request as draft July 6, 2026 14:42
@RevenueCat-Danger-Bot

RevenueCat-Danger-Bot commented Jul 6, 2026

Copy link
Copy Markdown
1 Error
🚫 This PR changes 686 lines of production Kotlin/Java code, over the 300-line limit. Split it into smaller PRs, or add the skip-pr-lines-changed-check label to bypass.

Generated by 🚫 Danger

@AlvaroBrey

Copy link
Copy Markdown
Contributor

@BugBot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

There are 3 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ 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 8063d3f. Configure here.

componentId = componentId,
type = type,
payload = variables.toJsonObject(),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

postMessage payload omits variables key

Medium Severity

PaywallWebViewController.postMessage documents that app data is sent under a variables field, but the bridge sets the transport payload to the flat map from variables.toJsonObject(). Web content expecting payload.variables will not see app-provided fields for custom message types.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8063d3f. Configure here.

) {
if (request.isForMainFrame && errorResponse.statusCode >= HTTP_ERROR_STATUS_MIN) {
onMainFrameLoadFailed()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Main-frame errors always trigger fallback

Medium Severity

New WebViewClient error handlers set loadFailed on any main-frame onReceivedError or HTTP status ≥ 400, with no check for benign codes like ERROR_CANCELLED. Once loadFailed is true and a schema fallback exists, the web view is replaced permanently even if the page actually loaded.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8063d3f. Configure here.

The native host will use declared component size only; drop bridge fit messages and resize handling.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants