Repository navigation
perry/ui WebView component: cross-platform native WebView for auth / payments / embedded pages #658
Description
Tracking issue consolidating discussion #651, issue #99, and the Windows-maps tangent under a single design.
Context
Three threads converge on the same primitive:
- Discussion Support cross-platform native WebView component for all mobile targets #651 (@nandy007): cross-platform native WebView for OAuth flows, payment providers (Stripe/PayPal), embedded admin/CMS, legacy web modules, partner integrations.
- Issue Notifications: WebView → native bridge for embedded-web notification API #99: Notifications WebView → native bridge — presumes WebView already exists.
- Windows MapView (MapView on Windows: WinUI MapControl backend (follow-up to #517) #559) cheaper-path discussion: WebView2 + Leaflet would deliver real maps in ~300 LOC vs the multi-day WinUI MapControl + XAML Islands path.
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
- Cookie sharing across multiple WebView instances — proposed default isolated per WebView; opt-in `groupId: string` if needed (v2).
- 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.
- POST navigation / form submits — defer; GET-only via `loadUrl` covers ~99% of OAuth flows.
- 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
feat(perry/ui): cross-platform WebView component (#658 Phase 1) (v0.5…
feat(perry/ui): WebView Phase 2 — Windows WebView2 backend (#658) (v0…
feat(perry/ui): WebView Phases 3+4+5 — Android, GTK4, web (#658) (v0.…
feat(perry/ui): WebView v2 follow-ups (#658) (v0.5.787)
proggeramlug commentedon May 10, 2026
Closing — Phases 1-5 + all v2 follow-ups shipped over 4 release cycles. Final state below.
Shipped
Total: ~3,560 LOC across 4 release cycles for a complete cross-platform primitive.
Platform support matrix
<iframe>TypeScript surface
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.
proggeramlug commentedon May 10, 2026
All five phases shipped — closing.
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.
docs(ui): WebView page covering Phases 1-5 + v2 follow-ups (#658)
fix(ci): memory json_churn limit + GTK4 webkit dep (v0.5.842)
refactor(link): extract the GTK4 pkg-config lib groups to linux_ui_li…
fix(link): stop --whole-archive'ing the stdlib on the Linux GTK4 UI p…