Skip to content

perry/ui WebView component: cross-platform native WebView for auth / payments / embedded pages #658

Closed

Description

@proggeramlug

Tracking issue consolidating discussion #651, issue #99, and the Windows-maps tangent under a single design.

Context

Three threads converge on the same primitive:

A focused WebView component unblocks all three.

Goals

  • Show a URL; observe navigations; intercept callback URLs (OAuth code-exchange).
  • Synchronous opt-in to block navigations (auth allowlist).
  • Async one-shot JS eval (document.cookie, localStorage.getItem).
  • All Perry-supported platforms: macOS, iOS, visionOS, Windows, Android, Linux/GTK4, tvOS / watchOS / web (stubs where the OS lacks a WebKit).

Non-goals (explicit)

  • Tauri/Electron-style native↔JS RPC bridge with bidirectional method registration.
  • Custom protocol/scheme handlers.
  • DevTools, file downloads/uploads, drag-drop.
  • WebGL / camera / mic / clipboard permission negotiation (default: blocked).
  • Persistent shared cookie jars across app sessions (default: ephemeral).
  • Service workers, WebRTC.

If a user wants Tauri/Electron, they should use Tauri/Electron. This is the "browser tab embedded inside a native widget tree" primitive.

TypeScript surface

```ts
import { WebView } from "perry/ui";

WebView({
url: string, // initial URL
allowedDomains?: string[], // hard navigation allowlist (security)
userAgent?: string, // override default
ephemeral?: boolean, // default true — no cookie persistence

onShouldNavigate?: (url: string) => boolean, // SYNC — return false to block
onLoaded?: (url: string) => void, // page finished loading
onError?: (errorCode: number, message: string) => void,

width?: number,
height?: number,
}) => Widget

// Imperative methods (handle-based, like Image/Button)
webviewLoadUrl(handle, url)
webviewReload(handle)
webviewGoBack(handle)
webviewGoForward(handle)
webviewEvaluateJs(handle, js, (result: string) => void)
webviewClearCookies(handle)
webviewCanGoBack(handle): boolean
```

Sync `onShouldNavigate` works because every backend's "should-load" hook is itself sync on the main thread (WKWebView `decidePolicyForNavigationAction`, WebView2 `NavigationStarting`, Android `shouldOverrideUrlLoading`, WebKitGTK `decide-policy`).

Per-platform backends

Platform Backend Phase
macOS `WKWebView` (AppKit) 1
iOS `WKWebView` (UIKit) 1
visionOS `WKWebView` 1 (best-effort)
Windows `WebView2` via `webview2-com` 2
Android `android.webkit.WebView` (JNI) 3
Linux/GTK4 `WebKitGTK 6.0` 4
tvOS Stub (Apple TV has no WKWebView) —
watchOS Stub (no WebKit) —
Web `<iframe>` or stub 5

Cookie/storage isolation

Default `ephemeral: true` — auth flows reusing a logged-in browser session is usually a footgun.

Platform Implementation
macOS / iOS / visionOS `WKWebViewConfiguration.websiteDataStore = .nonPersistent()`
Windows per-handle temp `userDataFolder` cleaned on widget destroy
Android `CookieManager.removeAllCookies()` + `WebStorage.deleteAllData()` on init
GTK4 `WebKitNetworkSession::new_ephemeral()`

FFI surface (~14 functions)

```
perry_ui_webview_create(url_ptr, width, height) -> i64
perry_ui_webview_set_user_agent(handle, ua_ptr)
perry_ui_webview_set_allowed_domains(handle, json_ptr)
perry_ui_webview_set_ephemeral(handle, ephemeral: i64)
perry_ui_webview_set_on_should_navigate(handle, closure: f64)
perry_ui_webview_set_on_loaded(handle, closure: f64)
perry_ui_webview_set_on_error(handle, closure: f64)
perry_ui_webview_load_url(handle, url_ptr)
perry_ui_webview_reload(handle)
perry_ui_webview_go_back(handle)
perry_ui_webview_go_forward(handle)
perry_ui_webview_can_go_back(handle) -> i64
perry_ui_webview_evaluate_js(handle, js_ptr, callback: f64)
perry_ui_webview_clear_cookies(handle)
```

Phased rollout

  • Phase 1 (macOS + iOS + visionOS) — single objc2-based codepath, ~600 LOC. Unblocks all WKWebView platforms.
  • Phase 2 (Windows) — WebView2 via `webview2-com`, ~400 LOC. Adds OAuth/payments to the desktop user.
  • Phase 3 (Android) — JNI hop to `android.webkit.WebView`, ~500 LOC.
  • Phase 4 (Linux/GTK4) — WebKitGTK 6.0, ~300 LOC. Already a build-time apt dep (`libwebkit2gtk-6.0-dev` listed in v0.5.771).
  • Phase 5 (tvOS / watchOS / Web) — stubs returning 0 handle; document the platform limitation.

Each phase ships independently — link-stability stubs for un-implemented backends keep cross-platform programs compiling.

Open questions / deferred

  1. Cookie sharing across multiple WebView instances — proposed default isolated per WebView; opt-in `groupId: string` if needed (v2).
  2. Notifications: WebView → native bridge for embedded-web notification API #99 Notifications bridge — explicitly deferred. Adding a JS→native message-handler surface is the slippery slope toward Tauri-style IPC and the user use cases here don't need it. Will reopen as a separate v2 design once the base primitive ships.
  3. POST navigation / form submits — defer; GET-only via `loadUrl` covers ~99% of OAuth flows.
  4. Web platform backend — `<iframe>` has different cross-origin restrictions; likely ships as stub-with-state.

Acceptance for Phase 1

  • `WebView({ url, onShouldNavigate, onLoaded })` creates a real WKWebView on macOS / iOS / visionOS, embedded in the parent widget tree.
  • OAuth flow demo: open Google's OAuth URL, intercept `https://localhost/callback\` via `onShouldNavigate`, extract the `code` param, return false to block the actual nav.
  • `webviewEvaluateJs(handle, "document.cookie", cb)` fires the callback with the cookie string.
  • Stubs for Windows / Android / GTK4 / tvOS / watchOS so cross-platform programs link.

cc @nandy007 from #651 — happy to take feedback on the API shape before Phase 2 platforms ship.

Activity

  1. proggeramlug commented on May 10, 2026

    @proggeramlug
    ContributorAuthor

    Closing — Phases 1-5 + all v2 follow-ups shipped over 4 release cycles. Final state below.

    Shipped

    Release Scope LOC
    v0.5.784 Phase 1 — macOS / iOS / visionOS via WKWebView ~1,600
    v0.5.785 Phase 2 — Windows via WebView2 / webview2-com 0.34 ~640
    v0.5.786 Phase 3 + 4 + 5 — Android, GTK4 (webkit6 0.4), web iframe ~680
    v0.5.787 v2 follow-ups — Android callbacks, persistent cookies, postMessage docs ~640

    Total: ~3,560 LOC across 4 release cycles for a complete cross-platform primitive.

    Platform support matrix

    Platform Backend Nav callbacks Persistent cookies Eval JS
    macOS WKWebView ✓ ✓ ✓ async
    iOS WKWebView ✓ ✓ ✓ async
    visionOS WKWebView ✓ ✓ ✓ async
    Windows WebView2 ✓ ✓ ✓ async
    Linux/GTK4 WebKitGTK 6.0 ✓ ✓ ✓ async
    Android android.webkit.WebView + PerryWebViewClient ✓ (v2-A) ⚠ best-effort (process-wide) ✓ async
    Web (wasm) <iframe> ⚠ same-origin only — (browser storage) ⚠ same-origin only
    tvOS Stub — — —
    watchOS Stub — — —

    TypeScript surface

    WebView({
      url, allowedDomains?, userAgent?, ephemeral?,
      onShouldNavigate?, onLoaded?, onError?, width?, height?
    })
    
    webviewLoadUrl / webviewReload / webviewGoBack / webviewGoForward /
    webviewCanGoBack / webviewEvaluateJs / webviewClearCookies

    Scope discipline maintained

    The closing commits maintain the deliberate scope boundary against Tauri/Electron-style app shells:

    The webviewBridge() opt-in API space is documented as a possible v3 if a real use case surfaces, but kept closed for now.

    Out-of-scope items still open as separate issues

    None — both follow-ups (#99 Notifications WebView bridge) and the original Windows-MapView-via-WebView2 discussion (#559) have been resolved or routed to their final form.

    Thanks @nandy007 for the original request in #651 — auth flows / payment redirects / embedded admin pages all work cross-platform now.

  2. proggeramlug commented on May 10, 2026

    @proggeramlug
    ContributorAuthor

    All five phases shipped — closing.

    • Phase 1 (macOS / iOS / visionOS WKWebView) — v0.5.784
    • Phase 2 (Windows WebView2 via webview2-com) — v0.5.785
    • Phase 3 (Android android.webkit.WebView via JNI) — v0.5.786
    • Phase 4 (Linux/GTK4 WebKitGTK 6.0) — v0.5.786
    • Phase 5 (web target via sandboxed iframe) — v0.5.786

    tvOS / watchOS keep link-stable stubs (Apple TV / Apple Watch don't ship WKWebView).

    Per-phase deferred bits (Android WebViewClient helper class for sync onShouldNavigate-with-cancel + real evaluateJs result delivery; GTK4 v4_8 ContentFit; web target cross-origin message bridge) are documented in the CHANGELOG and each is small enough to file independently if a user hits one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions