@livx.cc/appwrap 0.54.0 → 0.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/package.json +5 -4
  2. package/runtime/app/main-page.ts +0 -2
  3. package/runtime/app/shell/capabilities.manifest.ts +27 -92
  4. package/runtime/app/shell/config.ts +4 -0
  5. package/runtime/app/shell/env.ts +8 -1
  6. package/runtime/app/shell/handlers.ts +2 -2
  7. package/runtime/tests/bridge-response-delivery.test.ts +10 -1
  8. package/runtime/tests/env-scheme.test.ts +50 -0
  9. package/scripts/stage-assets.mjs +1 -1
  10. package/src/cli.ts +424 -621
  11. package/src/config.ts +8 -1
  12. package/src/handlers.ts +1 -1
  13. package/src/packs.ts +174 -0
  14. package/src/testing.ts +173 -0
  15. package/templates/module-pack/README.md +44 -0
  16. package/templates/module-pack/handler.ts +9 -0
  17. package/templates/module-pack/manifest.ts +27 -0
  18. package/templates/module-pack/native-src/__MODULE_NAME__/.gitkeep +0 -0
  19. package/runtime/app/shell/billing-offer.ts +0 -54
  20. package/runtime/app/shell/handlers-billing.ts +0 -662
  21. package/runtime/app/shell/handlers-health.ts +0 -178
  22. package/runtime/app/shell/handlers-widget.ts +0 -97
  23. package/runtime/modules-native/billing/App_Resources/iOS/src/AppwrapManageSubscriptions.swift +0 -44
  24. package/runtime/modules-native/health/App_Resources/Android/src/main/java/cc/livx/appwrap/HealthConnectBridge.kt +0 -74
  25. package/runtime/modules-native/widget/App_Resources/Android/src/main/java/cc/livx/appwrap/AppwrapWidgetProvider.kt +0 -118
  26. package/runtime/modules-native/widget/App_Resources/Android/src/main/res/drawable/appwrap_badge_bg.xml +0 -9
  27. package/runtime/modules-native/widget/App_Resources/Android/src/main/res/layout/appwrap_widget.xml +0 -123
  28. package/runtime/modules-native/widget/App_Resources/Android/src/main/res/values/appwrap_widget_colors.xml +0 -10
  29. package/runtime/modules-native/widget/App_Resources/Android/src/main/res/values/appwrap_widget_styles.xml +0 -67
  30. package/runtime/modules-native/widget/App_Resources/Android/src/main/res/values-night/appwrap_widget_colors.xml +0 -9
  31. package/runtime/modules-native/widget/App_Resources/Android/src/main/res/xml/appwrap_widget_info.xml +0 -17
  32. package/runtime/modules-native/widget/App_Resources/iOS/extensions/AppwrapWidget/AppwrapWidget.entitlements +0 -10
  33. package/runtime/modules-native/widget/App_Resources/iOS/extensions/AppwrapWidget/AppwrapWidget.swift +0 -275
  34. package/runtime/modules-native/widget/App_Resources/iOS/extensions/AppwrapWidget/Info.plist +0 -25
  35. package/runtime/modules-native/widget/App_Resources/iOS/extensions/AppwrapWidget/extension.json +0 -9
  36. package/runtime/modules-native/widget/App_Resources/iOS/src/AppwrapWidgetReload.swift +0 -18
  37. package/runtime/tests/billing-offer.test.ts +0 -61
  38. package/runtime-desktop/autotest/index.html +0 -77
  39. package/runtime-desktop/bridge-shim/build.sh +0 -9
  40. package/runtime-desktop/bridge-shim/content-entry.ts +0 -87
  41. package/runtime-desktop/bridge-shim/shim.ts +0 -132
  42. package/runtime-desktop/chrome/appwrap-browser-chrome.html +0 -185
  43. package/runtime-desktop/crates/webview-control/Cargo.lock +0 -330
  44. package/runtime-desktop/crates/webview-control/Cargo.toml +0 -28
  45. package/runtime-desktop/crates/webview-control/examples/host_harness.rs +0 -362
  46. package/runtime-desktop/crates/webview-control/src/embedded.rs +0 -138
  47. package/runtime-desktop/crates/webview-control/src/lib.rs +0 -100
  48. package/runtime-desktop/crates/webview-control/src/popup.rs +0 -522
  49. package/runtime-desktop/crates/webview-control/src/profiles.rs +0 -57
  50. package/runtime-desktop/crates/webview-control/src/wkwebview_backend.rs +0 -558
  51. package/runtime-desktop/src-tauri/Cargo.lock +0 -5474
  52. package/runtime-desktop/src-tauri/Cargo.toml +0 -45
  53. package/runtime-desktop/src-tauri/build.rs +0 -3
  54. package/runtime-desktop/src-tauri/capabilities/default.json +0 -6
  55. package/runtime-desktop/src-tauri/icons/icon.png +0 -0
  56. package/runtime-desktop/src-tauri/shell_config.json +0 -10
  57. package/runtime-desktop/src-tauri/src/biometrics_mac.rs +0 -138
  58. package/runtime-desktop/src-tauri/src/bridge_mac.rs +0 -424
  59. package/runtime-desktop/src-tauri/src/browser_tab.rs +0 -804
  60. package/runtime-desktop/src-tauri/src/main.rs +0 -1228
  61. package/runtime-desktop/src-tauri/src/notifications_mac.rs +0 -202
  62. package/runtime-desktop/src-tauri/src/oauth_mac.rs +0 -139
  63. package/runtime-desktop/src-tauri/src/plugin_host.rs +0 -522
  64. package/runtime-desktop/src-tauri/src/popup_mac.rs +0 -5
  65. package/runtime-desktop/src-tauri/src/push_mac.rs +0 -193
  66. package/runtime-desktop/src-tauri/src/server.rs +0 -156
  67. package/runtime-desktop/src-tauri/src/sidecar.rs +0 -398
  68. package/runtime-desktop/src-tauri/tauri.conf.json +0 -14
  69. package/src/desktop.ts +0 -300
  70. package/src/plugin/host.ts +0 -242
@@ -1,362 +0,0 @@
1
- //! Phase-2 host harness — proves a live WKWebView tears off (ChildOf pane -> TopLevel window) and
2
- //! docks back (-> ChildOf pane) with page + WKWebsiteDataStore session (localStorage marker) +
3
- //! WKScriptMessageHandler bridge all intact, driven THROUGH the embedded in-process API
4
- //! (`webview_control::embedded::Controller`) — driving-adapter (a), no socket.
5
- //!
6
- //! Run: `cargo run --example host_harness` (macOS; add `-- --hold` to keep windows up for a look).
7
- //!
8
- //! It is a minimal bare-AppKit host: it owns a pane NSView inside a plain NSWindow (mirroring what a
9
- //! Tauri pane / unclaw-app pane provides), serves a loopback page from an in-process HTTP server
10
- //! (opaque about:/data: origins can't use localStorage), then runs the assertion sequence and exits
11
- //! non-zero on any failure. The AppKit main runloop is pumped manually so each async step
12
- //! (callAsyncJavaScript / page load / bridge post) completes before the next.
13
-
14
- use std::cell::RefCell;
15
- use std::ffi::c_void;
16
- use std::io::{Read, Write};
17
- use std::net::TcpListener;
18
- use std::rc::Rc;
19
- use std::sync::mpsc::{channel, Receiver};
20
- use std::time::{Duration, Instant};
21
-
22
- use objc2::rc::Retained;
23
- use objc2::MainThreadMarker;
24
- use objc2_app_kit::{
25
- NSApplication, NSApplicationActivationPolicy, NSBackingStoreType, NSView, NSWindow,
26
- NSWindowStyleMask,
27
- };
28
- use objc2_foundation::{
29
- NSDate, NSDefaultRunLoopMode, NSPoint, NSRect, NSRunLoop, NSSize, NSString,
30
- };
31
-
32
- use webview_control::embedded::Controller;
33
- use webview_control::{CreateSpec, Mount, ViewHandle};
34
-
35
- /// Pump the AppKit main runloop for up to `secs`, returning early once `done()` is true.
36
- fn pump_until(secs: f64, mut done: impl FnMut() -> bool) {
37
- let rl = NSRunLoop::currentRunLoop();
38
- let deadline = Instant::now() + Duration::from_secs_f64(secs);
39
- while Instant::now() < deadline {
40
- if done() {
41
- return;
42
- }
43
- let until = NSDate::dateWithTimeIntervalSinceNow(0.05);
44
- unsafe { rl.runMode_beforeDate(NSDefaultRunLoopMode, &until) };
45
- }
46
- }
47
-
48
- fn pump(secs: f64) {
49
- pump_until(secs, || false);
50
- }
51
-
52
- /// Synchronous eval through the embedded Controller: fire callAsyncJavaScript and pump until the
53
- /// completion block lands (or timeout). Returns the RAW return string.
54
- fn eval(ctrl: &Controller, mtm: MainThreadMarker, id: &str, js: &str) -> Result<String, String> {
55
- let slot: Rc<RefCell<Option<Result<String, String>>>> = Rc::new(RefCell::new(None));
56
- let s2 = slot.clone();
57
- ctrl.eval_async(mtm, id, js.to_string(), move |r| *s2.borrow_mut() = Some(r));
58
- pump_until(8.0, || slot.borrow().is_some());
59
- let out = slot.borrow_mut().take();
60
- out.unwrap_or(Err("eval timed out".into()))
61
- }
62
-
63
- /// Start a throwaway loopback HTTP server; returns its `http://127.0.0.1:PORT/` URL. The page just
64
- /// carries a stable title — the session marker is written via localStorage by the harness.
65
- fn start_loopback() -> String {
66
- start_loopback_titled("HOST-HARNESS-PAGE")
67
- }
68
-
69
- /// Start a throwaway loopback HTTP server carrying `title` as its `<title>`; returns its URL.
70
- fn start_loopback_titled(title: &str) -> String {
71
- let listener = TcpListener::bind("127.0.0.1:0").expect("bind loopback");
72
- let addr = listener.local_addr().unwrap();
73
- let title = title.to_string();
74
- std::thread::spawn(move || {
75
- let body = format!(
76
- "<!doctype html><html><head><title>{title}</title></head>\
77
- <body style=\"margin:0;background:#0b5;color:#fff;font:700 40px system-ui;\
78
- display:flex;align-items:center;justify-content:center;height:100vh\">\
79
- <div id=\"tag\">HOST-HARNESS-LIVE</div></body></html>"
80
- );
81
- for stream in listener.incoming() {
82
- let Ok(mut s) = stream else { continue };
83
- let mut buf = [0u8; 1024];
84
- let _ = s.read(&mut buf);
85
- let resp = format!(
86
- "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
87
- body.len(),
88
- body
89
- );
90
- let _ = s.write_all(resp.as_bytes());
91
- }
92
- });
93
- format!("http://{addr}/")
94
- }
95
-
96
- /// Read `location.href` once (pumping until the eval completes).
97
- fn href(ctrl: &Controller, mtm: MainThreadMarker, id: &str) -> String {
98
- eval(ctrl, mtm, id, "return location.href;").unwrap_or_default()
99
- }
100
-
101
- /// Issue a nav op (idempotent — navigate/go_back/go_forward/reload can all be safely re-issued) and
102
- /// poll `location.href` until it contains `needle` or the deadline elapses. Re-issuing each iteration
103
- /// makes it robust to a WKWebView dropping a single async load request. Returns the last href.
104
- fn nav_settle(
105
- ctrl: &Controller,
106
- mtm: MainThreadMarker,
107
- id: &str,
108
- needle: &str,
109
- secs: f64,
110
- mut issue: impl FnMut(),
111
- ) -> String {
112
- let deadline = Instant::now() + Duration::from_secs_f64(secs);
113
- let mut last = String::new();
114
- while Instant::now() < deadline {
115
- issue();
116
- pump(0.4);
117
- last = href(ctrl, mtm, id);
118
- if last.contains(needle) {
119
- return last;
120
- }
121
- }
122
- last
123
- }
124
-
125
- fn main() {
126
- let mtm = MainThreadMarker::new().expect("host_harness must run on the main thread");
127
-
128
- // Scratch profile dir so we don't touch the real appwrap-desktop store.
129
- let scratch = std::env::temp_dir().join(format!("webview-control-harness-{}", std::process::id()));
130
- std::env::set_var("APPWRAP_PROFILE_DIR", &scratch);
131
-
132
- // AppKit host app (accessory: no Dock icon; we pump the runloop manually rather than app.run()).
133
- let app = NSApplication::sharedApplication(mtm);
134
- app.setActivationPolicy(NSApplicationActivationPolicy::Accessory);
135
- let _ = app.finishLaunching();
136
-
137
- // The HOST-owned pane: a plain NSWindow whose content view is the container the tab docks into.
138
- let pane_rect = NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(900.0, 640.0));
139
- let style = NSWindowStyleMask::Titled | NSWindowStyleMask::Closable | NSWindowStyleMask::Resizable;
140
- let pane_window = unsafe {
141
- NSWindow::initWithContentRect_styleMask_backing_defer(
142
- mtm.alloc::<NSWindow>(),
143
- pane_rect,
144
- style,
145
- NSBackingStoreType::Buffered,
146
- false,
147
- )
148
- };
149
- unsafe {
150
- pane_window.setReleasedWhenClosed(false);
151
- pane_window.setTitle(&NSString::from_str("[host pane]"));
152
- }
153
- let pane = NSView::initWithFrame(mtm.alloc::<NSView>(), pane_rect);
154
- pane_window.setContentView(Some(&pane));
155
- pane_window.center();
156
- pane_window.makeKeyAndOrderFront(None);
157
- let pane_handle = unsafe { ViewHandle::from_ptr(Retained::as_ptr(&pane) as *mut c_void) };
158
-
159
- let page_url = start_loopback();
160
- let id = "harness-tab";
161
- let ctrl = Controller::new();
162
-
163
- // Bridge sink: the crate hands us RAW page->host payloads; capture them to prove the
164
- // WKScriptMessageHandler survives reparenting.
165
- let (tx, rx): (_, Receiver<String>) = channel();
166
- let sink = Box::new(move |raw: String| {
167
- let _ = tx.send(raw);
168
- });
169
-
170
- // (1) Create the tab as a CHILD of the host pane — an embedded in-app browser tab.
171
- println!("== create_webview ChildOf(pane) id={id} url={page_url}");
172
- let created = ctrl.create_webview(
173
- mtm,
174
- CreateSpec {
175
- label: id.to_string(),
176
- url: page_url.clone(),
177
- profile_id: "harness".to_string(),
178
- init_js: String::new(),
179
- mount: Mount::ChildOf(pane_handle),
180
- },
181
- sink,
182
- );
183
- match &created {
184
- Ok(c) => println!(" created: persistent={} store={}", c.persistent, c.store_uuid),
185
- Err(e) => {
186
- eprintln!("FAIL: create_webview: {e}");
187
- std::process::exit(1);
188
- }
189
- }
190
- pump(3.0); // let the page load
191
-
192
- // (2) Write the session marker + capture page identity while CHILD.
193
- let set = eval(
194
- &ctrl,
195
- mtm,
196
- id,
197
- "localStorage.setItem('marker','HOST-HARNESS-SESSION-77'); \
198
- return JSON.stringify({href:location.href,title:document.title,marker:localStorage.getItem('marker')});",
199
- );
200
- println!("== child, marker set: {set:?}");
201
-
202
- // (3) Bridge ping while CHILD.
203
- let _ = eval(&ctrl, mtm, id, "webkit.messageHandlers.bridgeShim.postMessage('PING-CHILD'); return 'posted';");
204
- pump(0.5);
205
-
206
- // (4) TEAR OFF -> TopLevel window.
207
- println!("== set_mount TopLevel (tear off)");
208
- if !ctrl.set_mount(mtm, id, Mount::TopLevel) {
209
- eprintln!("FAIL: set_mount TopLevel returned false");
210
- std::process::exit(1);
211
- }
212
- pump(1.5);
213
-
214
- // (5) DOCK BACK -> ChildOf(pane).
215
- println!("== set_mount ChildOf(pane) (dock back)");
216
- if !ctrl.set_mount(mtm, id, Mount::ChildOf(pane_handle)) {
217
- eprintln!("FAIL: set_mount ChildOf returned false");
218
- std::process::exit(1);
219
- }
220
- pump(1.5);
221
-
222
- // (6) After child->toplevel->child: read page identity + marker back, and ping the bridge again.
223
- let after = eval(
224
- &ctrl,
225
- mtm,
226
- id,
227
- "return JSON.stringify({href:location.href,title:document.title,marker:localStorage.getItem('marker')});",
228
- );
229
- println!("== after reparent cycle, read-back: {after:?}");
230
- let _ = eval(&ctrl, mtm, id, "webkit.messageHandlers.bridgeShim.postMessage('PING-AFTER'); return 'posted';");
231
- pump(0.5);
232
-
233
- // ---- assertions ----
234
- let after = match after {
235
- Ok(s) => s,
236
- Err(e) => {
237
- eprintln!("FAIL: read-back eval error after reparent: {e}");
238
- std::process::exit(1);
239
- }
240
- };
241
- let page_ok = after.contains("HOST-HARNESS-PAGE"); // document.title preserved (page not reloaded/lost)
242
- let session_ok = after.contains("HOST-HARNESS-SESSION-77"); // localStorage marker survived
243
- let pings: Vec<String> = rx.try_iter().collect();
244
- let bridge_ok = pings.iter().any(|p| p == "PING-CHILD") && pings.iter().any(|p| p == "PING-AFTER");
245
-
246
- println!("\n---- Phase-2 embedded-API reparent assertions ----");
247
- println!("page survived (title HOST-HARNESS-PAGE): {page_ok}");
248
- println!("session survived (marker 77): {session_ok}");
249
- println!("bridge survived (pings {:?}): {bridge_ok}", pings);
250
-
251
- // ---- IDEMPOTENT DOUBLE-INSTALL (bridgeShim) ----
252
- // Simulate a SECOND plugin attaching onMessage to the SAME window. Before the fix, a second
253
- // addScriptMessageHandler(name:"bridgeShim") on the same UCC threw NSInvalidArgumentException =
254
- // hard crash. install_message_handler now does remove-then-add, so this must NOT crash and the
255
- // bridge must still route to the newly-installed sink. Runs AFTER the original-sink assertions
256
- // above (re-install replaces the sink, which is fine from here on).
257
- println!("\n== IDEMPOTENCY: re-install bridgeShim on the live webview (second-plugin attach)");
258
- let (tx2, rx2): (_, Receiver<String>) = channel();
259
- let sink2 = Box::new(move |raw: String| { let _ = tx2.send(raw); });
260
- if let Err(e) = ctrl.reinstall_message_handler(mtm, id, sink2) {
261
- eprintln!("FAIL: reinstall_message_handler errored: {e}");
262
- std::process::exit(1);
263
- }
264
- let _ = eval(&ctrl, mtm, id, "webkit.messageHandlers.bridgeShim.postMessage('PING-REINSTALL'); return 'posted';");
265
- pump(0.5);
266
- let reinstall_pings: Vec<String> = rx2.try_iter().collect();
267
- let idempotent_ok = reinstall_pings.iter().any(|p| p == "PING-REINSTALL");
268
- println!(" no crash on double-install; post-reinstall pings {reinstall_pings:?} (routed={idempotent_ok})");
269
-
270
- // ---- NAV ops (go_back / go_forward / reload via Controller) ----
271
- // The tab is docked ChildOf(pane) on page 1. Navigate to a distinct page 2, then walk history.
272
- let page1_href = href(&ctrl, mtm, id);
273
- let page2_url = start_loopback_titled("HOST-HARNESS-PAGE-2");
274
- println!("\n== NAV: navigate -> page2 {page2_url}");
275
- let at2 = nav_settle(&ctrl, mtm, id, &page2_url, 10.0, || { let _ = ctrl.navigate(mtm, id, &page2_url); });
276
- let nav_fwd_ok = at2.contains(&page2_url);
277
- println!(" at page2: {at2:?} (ok={nav_fwd_ok})");
278
-
279
- println!("== NAV: go_back -> page1");
280
- let back1 = nav_settle(&ctrl, mtm, id, &page1_href, 10.0, || { let _ = ctrl.go_back(mtm, id); });
281
- let nav_back_ok = back1 == page1_href && !page1_href.is_empty();
282
- println!(" back at page1: {back1:?} (ok={nav_back_ok})");
283
-
284
- println!("== NAV: go_forward -> page2");
285
- let fwd2 = nav_settle(&ctrl, mtm, id, &page2_url, 10.0, || { let _ = ctrl.go_forward(mtm, id); });
286
- let nav_fwd2_ok = fwd2.contains(&page2_url);
287
- println!(" forward at page2: {fwd2:?} (ok={nav_fwd2_ok})");
288
-
289
- println!("== NAV: reload (still page2, title present)");
290
- let reloaded = nav_settle(&ctrl, mtm, id, &page2_url, 10.0, || { let _ = ctrl.reload(mtm, id); });
291
- let title2 = eval(&ctrl, mtm, id, "return document.title;").unwrap_or_default();
292
- let reload_ok = reloaded.contains(&page2_url) && title2.contains("HOST-HARNESS-PAGE-2");
293
- println!(" after reload: href={reloaded:?} title={title2:?} (ok={reload_ok})");
294
- let nav_ok = nav_fwd_ok && nav_back_ok && nav_fwd2_ok && reload_ok;
295
-
296
- // ---- PROFILE SWAP: a second webview on a DIFFERENT profile → different store uuid ----
297
- let first_store = created.as_ref().map(|c| c.store_uuid.clone()).unwrap_or_default();
298
- let id_b = "harness-tab-b";
299
- println!("\n== PROFILE SWAP: create id={id_b} profile=harnessB ChildOf(same pane)");
300
- let created_b = ctrl.create_webview(
301
- mtm,
302
- CreateSpec {
303
- label: id_b.to_string(),
304
- url: page_url.clone(),
305
- profile_id: "harnessB".to_string(),
306
- init_js: String::new(),
307
- mount: Mount::ChildOf(pane_handle),
308
- },
309
- Box::new(|_raw| {}),
310
- );
311
- let store_b = match &created_b {
312
- Ok(c) => { println!(" harnessB store={}", c.store_uuid); c.store_uuid.clone() }
313
- Err(e) => { eprintln!("FAIL: create harnessB: {e}"); std::process::exit(1); }
314
- };
315
- let profile_swap_ok = !first_store.is_empty() && store_b != first_store;
316
- println!(" store isolation: harness={first_store} harnessB={store_b} (differ={profile_swap_ok})");
317
- let _ = ctrl.close(mtm, id_b);
318
- pump(0.3);
319
-
320
- println!("\n---- nav + profile-swap assertions ----");
321
- println!("nav ops (fwd/back/fwd/reload): {nav_ok}");
322
- println!("profile swap (distinct store uuid): {profile_swap_ok}");
323
-
324
- if std::env::args().any(|a| a == "--hold") {
325
- println!("(--hold) leaving windows up 20s for inspection…");
326
- pump(20.0);
327
- }
328
- // ---- zombie-close gate ----
329
- // The tab is currently docked ChildOf(pane) (step 5/6). BEFORE close it is a subview of the host
330
- // pane; AFTER close() the pane MUST have ZERO subviews — i.e. the WKWebView is actually gone from
331
- // the host view tree, not left alive+visible as a zombie. This machine-gates the Phase-2 HIGH bug
332
- // where close() only called window.close() and never removeFromSuperview() on the embedded webview.
333
- let before_close = pane.subviews().count();
334
- println!("\n== host pane subview count BEFORE close: {before_close} (expect 1: the docked tab)");
335
- let _ = ctrl.close(mtm, id);
336
- pump(0.5); // let AppKit settle the view-tree mutation
337
- let after_close = pane.subviews().count();
338
- println!("== host pane subview count AFTER close: {after_close} (expect 0: tab detached)");
339
- let pane_empty_ok = after_close == 0;
340
-
341
- println!("pane empty after close (no zombie webview): {pane_empty_ok}");
342
-
343
- println!("bridge idempotent re-install (no crash, routes): {idempotent_ok}");
344
- if !idempotent_ok {
345
- eprintln!("\nFAIL: bridge did not route after idempotent double-install of bridgeShim.");
346
- }
347
-
348
- if page_ok && session_ok && bridge_ok && pane_empty_ok && nav_ok && profile_swap_ok && idempotent_ok {
349
- println!("\nPASS: live webview reparented child->toplevel->child via EMBEDDED API; page+session+bridge intact; nav ops (back/forward/reload) + profile-swap (distinct store) verified; host pane empty after close.");
350
- } else {
351
- if !nav_ok { eprintln!("\nFAIL: nav ops (go_back/go_forward/reload) did not land as expected."); }
352
- if !profile_swap_ok { eprintln!("\nFAIL: profile swap did not yield a distinct store uuid."); }
353
- if !pane_empty_ok {
354
- eprintln!(
355
- "\nFAIL: ZOMBIE WEBVIEW — host pane still has {after_close} subview(s) after close(); \
356
- the embedded WKWebView was not removed from the host view tree."
357
- );
358
- }
359
- eprintln!("\nFAIL: one or more invariants did not survive reparent.");
360
- std::process::exit(1);
361
- }
362
- }
@@ -1,138 +0,0 @@
1
- //! Driving adapter (a): the in-process Rust API for EMBEDDED hosts (para-chat / unclaw-app).
2
- //!
3
- //! Same command surface as the control-channel adapter (bridge_mac.rs, driving adapter (b)) but
4
- //! with NO socket, NO JSON framing — a plain Rust object over the one `WebviewControl` contract.
5
- //! An embedded host that links this crate creates an in-app browser tab as `ChildOf(paneView)` and
6
- //! tears it out / docks it back with `set_mount` — the same live WKWebView, session + bridge intact.
7
- //!
8
- //! Threading: every method takes a `MainThreadMarker` and runs synchronously on the AppKit main
9
- //! thread. The facade deliberately has NO built-in `run_on_main_thread` — the host owns marshalling
10
- //! (Tauri's `run_on_main_thread`, or being called from the main runloop). WKWebView/NSView are not
11
- //! thread-safe, and the crate must not assume a Tauri `AppHandle`.
12
-
13
- use objc2::rc::Retained;
14
- use objc2::MainThreadMarker;
15
- use objc2_web_kit::WKWebView;
16
-
17
- use crate::wkwebview_backend as backend;
18
- use crate::{CreateResult, CreateSpec, MessageSink, Mount, WebviewInfo};
19
-
20
- /// In-process facade over the webview primitive. Stateless — the webview registry is a crate-level
21
- /// static — so it is a zero-cost handle the host can construct freely.
22
- #[derive(Clone, Copy, Default)]
23
- pub struct Controller;
24
-
25
- impl Controller {
26
- pub fn new() -> Self {
27
- Controller
28
- }
29
-
30
- /// Create a webview mounted per `spec.mount` (`ChildOf(pane)` for an embedded tab). `on_message`
31
- /// receives RAW page->host payloads (`webkit.messageHandlers.bridgeShim.postMessage`).
32
- pub fn create_webview(
33
- &self,
34
- mtm: MainThreadMarker,
35
- spec: CreateSpec,
36
- on_message: MessageSink,
37
- ) -> Result<CreateResult, String> {
38
- backend::create_webview(mtm, spec, on_message)
39
- }
40
-
41
- /// Tear-off / dock: move the live webview between `TopLevel` and `ChildOf(pane)`. False if unknown.
42
- pub fn set_mount(&self, mtm: MainThreadMarker, id: &str, mount: Mount) -> bool {
43
- backend::set_mount(mtm, id, mount)
44
- }
45
-
46
- /// Navigate the webview `id` to `url`. Err if `id` is unknown/closed.
47
- pub fn navigate(&self, _mtm: MainThreadMarker, id: &str, url: &str) -> Result<(), String> {
48
- let wk = self.webview(id)?;
49
- unsafe { backend::navigate(Retained::as_ptr(&wk) as *mut WKWebView, url) }
50
- }
51
-
52
- /// Navigate back in webview `id`'s history. Err if `id` is unknown/closed.
53
- pub fn go_back(&self, _mtm: MainThreadMarker, id: &str) -> Result<(), String> {
54
- let wk = self.webview(id)?;
55
- unsafe { backend::go_back(Retained::as_ptr(&wk) as *mut WKWebView) };
56
- Ok(())
57
- }
58
-
59
- /// Navigate forward in webview `id`'s history. Err if `id` is unknown/closed.
60
- pub fn go_forward(&self, _mtm: MainThreadMarker, id: &str) -> Result<(), String> {
61
- let wk = self.webview(id)?;
62
- unsafe { backend::go_forward(Retained::as_ptr(&wk) as *mut WKWebView) };
63
- Ok(())
64
- }
65
-
66
- /// Reload webview `id`. Err if `id` is unknown/closed.
67
- pub fn reload(&self, _mtm: MainThreadMarker, id: &str) -> Result<(), String> {
68
- let wk = self.webview(id)?;
69
- unsafe { backend::reload(Retained::as_ptr(&wk) as *mut WKWebView) };
70
- Ok(())
71
- }
72
-
73
- /// Fire-and-forget JS injection into webview `id`. Err if `id` is unknown/closed.
74
- pub fn inject_script(&self, _mtm: MainThreadMarker, id: &str, js: &str) -> Result<(), String> {
75
- let wk = self.webview(id)?;
76
- unsafe { backend::inject_script(Retained::as_ptr(&wk) as *mut WKWebView, js) };
77
- Ok(())
78
- }
79
-
80
- /// CSP-immune eval (callAsyncJavaScript, `js` is a function body). Delivers the RAW return
81
- /// string via `on_done` on the main thread. Err (via `on_done`) if `id` is unknown/closed.
82
- pub fn eval_async(
83
- &self,
84
- _mtm: MainThreadMarker,
85
- id: &str,
86
- js: String,
87
- on_done: impl FnOnce(Result<String, String>) + 'static,
88
- ) {
89
- match self.webview(id) {
90
- Ok(wk) => unsafe { backend::eval_async(Retained::as_ptr(&wk) as *mut WKWebView, js, on_done) },
91
- Err(e) => on_done(Err(e)),
92
- }
93
- }
94
-
95
- /// Snapshot webview `id` to PNG bytes via `on_done` on the main thread.
96
- pub fn snapshot(
97
- &self,
98
- _mtm: MainThreadMarker,
99
- id: &str,
100
- on_done: impl FnOnce(Result<Vec<u8>, String>) + 'static,
101
- ) {
102
- match self.webview(id) {
103
- Ok(wk) => unsafe { backend::snapshot(Retained::as_ptr(&wk) as *mut WKWebView, on_done) },
104
- Err(e) => on_done(Err(e)),
105
- }
106
- }
107
-
108
- /// Close + drop the webview `id` (releases its window + view). False if unknown.
109
- pub fn close(&self, _mtm: MainThreadMarker, id: &str) -> bool {
110
- backend::close(id)
111
- }
112
-
113
- /// Live (non-closed) webviews.
114
- pub fn list_webviews(&self) -> Vec<WebviewInfo> {
115
- backend::list_webviews()
116
- }
117
-
118
- /// Re-install the bridgeShim message handler on an existing webview's UCC. Mirrors a second
119
- /// plugin attaching `onMessage` to the same window. Idempotent (remove-then-add) — proves the
120
- /// double-install crash (NSInvalidArgumentException) is gone. Test/harness surface only.
121
- #[doc(hidden)]
122
- pub fn reinstall_message_handler(
123
- &self,
124
- mtm: MainThreadMarker,
125
- id: &str,
126
- on_message: crate::MessageSink,
127
- ) -> Result<(), String> {
128
- let wk = self.webview(id)?;
129
- unsafe {
130
- backend::install_message_handler(Retained::as_ptr(&wk) as *mut WKWebView, mtm, on_message)
131
- };
132
- Ok(())
133
- }
134
-
135
- fn webview(&self, id: &str) -> Result<Retained<WKWebView>, String> {
136
- backend::profile_webview(id).ok_or_else(|| format!("no webview {id}"))
137
- }
138
- }
@@ -1,100 +0,0 @@
1
- //! webview-control — appwrap's generalized, automation-agnostic native webview primitive.
2
- //!
3
- //! Carved out of `src-tauri/src/bridge_mac.rs` (Phase 1). It owns the parts that MUST be native +
4
- //! in-process + appwrap-owned because `WKWebsiteDataStore` is immutable post-webview-creation:
5
- //! per-profile persistent+isolated stores, manual NSWindow+WKWebView creation, CSP-immune eval,
6
- //! snapshot, the `bridgeShim` WKScriptMessageHandler, and the SSO-popup delegate.
7
- //!
8
- //! Seam purity: the primitive emits RAW page payloads (`MessageSink`) and RAW eval/snapshot results
9
- //! (per-call `on_done` callbacks). It holds NO bod-browser protocol, transport `Sender`, or
10
- //! `{type:"pageMsg"}` / `{id,result}` framing — those live in the control-channel adapter
11
- //! (`bridge_mac.rs`). The `WkWebViewBackend` is macOS-only; a `WebKitGtkBackend` is a future slot.
12
- //!
13
- //! Two driving adapters over ONE contract:
14
- //! - (a) [`embedded::Controller`] — in-process Rust API for EMBEDDED hosts (para-chat / unclaw-app);
15
- //! creates tabs as [`Mount::ChildOf`] a host pane and tears off / docks with [`set_mount`].
16
- //! - (b) the control-channel socket (`bridge_mac.rs`) — standalone / MCP; only ever [`Mount::TopLevel`].
17
- //!
18
- //! Cross-repo consumption (R1, resolved Phase 2): external hosts pin this crate as a **git dependency
19
- //! on a tagged appwrap rev** — `webview-control = { git = ".../appwrap", tag = "webview-control-vX" }`
20
- //! (no crates.io publish). appwrap-generated apps (unclaw-app) already get it via the whole-tree
21
- //! `native-desktop` copy; para-chat's hand-maintained `src-tauri` adds the git dep directly. objc2*
22
- //! versions come pinned by the tagged rev so the `*mut WKWebView`/`*mut NSView` pointers that cross
23
- //! this boundary never hit an ABI mismatch.
24
-
25
- #[cfg(target_os = "macos")]
26
- pub mod embedded;
27
- #[cfg(target_os = "macos")]
28
- pub mod popup;
29
- #[cfg(target_os = "macos")]
30
- mod profiles;
31
- #[cfg(target_os = "macos")]
32
- mod wkwebview_backend;
33
-
34
- use std::ffi::c_void;
35
-
36
- /// Opaque handle to a host-owned `NSView` that a webview can be mounted inside (Phase 2).
37
- ///
38
- /// # Safety contract
39
- /// The wrapped pointer MUST be a valid `NSView*` (or subclass) for the ENTIRE time a webview is
40
- /// mounted in it. The caller guarantees, and the crate relies on, all of:
41
- /// - **Validity:** a live `NSView` created and retained by the *host* (e.g. a Tauri pane's
42
- /// `ns_view()` / an AppKit container). Never a dangling, freed, or non-`NSView` pointer.
43
- /// - **Main-thread only:** the handle is created, passed, and dereferenced ONLY on the AppKit main
44
- /// thread. `ViewHandle` is intentionally `!Send`/`!Sync` — it must not cross threads.
45
- /// - **Host-owned lifetime:** the host owns the pane and MUST NOT drop/free it while a webview is
46
- /// mounted in it. Reparent the webview out (`set_mount(TopLevel)`) or `close` it BEFORE tearing
47
- /// down the pane. The crate holds only a borrow-strength AppKit subview link, not ownership of
48
- /// the host pane.
49
- /// - **Laid-out bounds (Phase-3 landmine a):** fetch the `NSView*` and mount `ChildOf` it only
50
- /// AFTER the pane has been laid out (non-zero `bounds`). The webview is framed to the pane's
51
- /// bounds at mount; a zero-bounds pane (handle taken before layout) renders BLANK until a resize.
52
- /// Take the handle post-layout, on the main thread.
53
- /// - **objc2 ABI match (Phase-3 landmine b):** a host with its OWN `wry`/`objc2` tree (e.g.
54
- /// para-chat's `src-tauri`) MUST resolve `objc2*` to the SAME `0.6.x` this crate is built against
55
- /// (pinned by the tagged rev). A divergent objc2 makes the `*mut NSView` pointer cast UB in the
56
- /// host's tree — pin/dedupe objc2 across the workspace, don't just add the git dep.
57
- #[derive(Clone, Copy)]
58
- pub struct ViewHandle(*mut c_void);
59
-
60
- impl ViewHandle {
61
- /// Wrap a raw host `NSView*`. See the type-level safety contract — the caller upholds validity,
62
- /// main-thread affinity, and host-owned lifetime.
63
- ///
64
- /// # Safety
65
- /// `ptr` must be a valid `NSView*` per the [`ViewHandle`] contract.
66
- pub unsafe fn from_ptr(ptr: *mut c_void) -> Self {
67
- ViewHandle(ptr)
68
- }
69
-
70
- /// The raw `NSView*`. Main-thread only.
71
- pub fn as_ptr(self) -> *mut c_void {
72
- self.0
73
- }
74
- }
75
-
76
- /// Where a webview lives in the AppKit view tree (Phase 2 tear-off / dock).
77
- /// - `TopLevel` — the webview owns/fills its own `NSWindow` content view (the standalone/MCP shape;
78
- /// the only mount the control-channel adapter ever requests, so its behavior is unchanged).
79
- /// - `ChildOf(handle)` — the webview is a subview of a host-owned pane `NSView`; the same live
80
- /// `WKWebView` (page + `WKWebsiteDataStore` session + bridge) survives moving between the two.
81
- #[derive(Clone, Copy)]
82
- pub enum Mount {
83
- TopLevel,
84
- ChildOf(ViewHandle),
85
- }
86
-
87
- // The Phase-1 primitive surface — a flat, transport-free command set the control-channel adapter
88
- // drives — plus the Phase-2 reparent op (`set_mount`) and the embedded facade (`embedded::Controller`).
89
- // See ARCHITECTURE.md for the full contract.
90
- #[cfg(target_os = "macos")]
91
- pub use wkwebview_backend::{
92
- close, create_profile_window, create_webview, devtools_enabled, eval_async, go_back, go_forward,
93
- inject_script, install_message_handler, install_named_message_handler, list_webviews, navigate,
94
- profile_webview, reload,
95
- set_inspectable, set_mount, snapshot, user_close, CreateResult, CreateSpec, MessageSink,
96
- WebviewInfo,
97
- };
98
-
99
- #[cfg(target_os = "macos")]
100
- pub use profiles::list as list_profiles;