@livx.cc/appwrap 0.61.17 → 0.61.19
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.
package/package.json
CHANGED
|
@@ -18,6 +18,8 @@ const KVO_KEYS = ['URL', 'title', 'canGoBack', 'canGoForward', 'loading'];
|
|
|
18
18
|
* webview.nav {op:'back'|'forward'|'reload'|'go', url?}
|
|
19
19
|
* webview.hide / webview.show {top?,bottom?} — keep state, reveal/cover the page beneath (show re-insets)
|
|
20
20
|
* webview.close — tear down; emits `webview.closed`
|
|
21
|
+
* webview.cookies {cookies: Cookie[]} — write cookies into the overlay's (persistent) jar; resolves {set}
|
|
22
|
+
* once all are stored (Bare Remote's opt-in "sign in like the Mac")
|
|
21
23
|
* Events: `webview.state` {url,title,canGoBack,canGoForward,loading} on every navigation change.
|
|
22
24
|
*
|
|
23
25
|
* Swipe back/forward on, target=_blank/window.open loads in place (shared ios-ui-delegate), keyboard +
|
|
@@ -26,6 +28,14 @@ const KVO_KEYS = ['URL', 'title', 'canGoBack', 'canGoForward', 'loading'];
|
|
|
26
28
|
* WebView's box (re-applied when it re-lays out), cookies via the process-wide CookieManager (persistent),
|
|
27
29
|
* getUserMedia via the shared permission handler, _blank in place (no multiple-window support).
|
|
28
30
|
*/
|
|
31
|
+
/** A cookie as data (Bare's desktop `remote.cookies` shape); `expires` = unix seconds, absent = session cookie. */
|
|
32
|
+
type Cookie = { name: string; value: string; domain: string; path?: string; secure?: boolean; httpOnly?: boolean; expires?: number | null; sameSite?: string | null };
|
|
33
|
+
const cookieList = (p: { cookies?: Cookie[] } | null): Cookie[] => {
|
|
34
|
+
const l = p?.cookies;
|
|
35
|
+
if (!Array.isArray(l)) throw err('NATIVE_ERROR', 'webview.cookies: cookies[] required');
|
|
36
|
+
return l.filter((c) => c && c.name && c.domain);
|
|
37
|
+
};
|
|
38
|
+
|
|
29
39
|
export function registerWebViewHandlers(): void {
|
|
30
40
|
if (isIOS) registerIos();
|
|
31
41
|
else if (isAndroid) registerAndroid();
|
|
@@ -160,6 +170,31 @@ function registerIos(): void {
|
|
|
160
170
|
return { open: !!wv };
|
|
161
171
|
}));
|
|
162
172
|
bridge.register('webview.close', () => onMain(() => { destroy(); }));
|
|
173
|
+
|
|
174
|
+
bridge.register('webview.cookies', (p: { cookies?: Cookie[] } | null) => {
|
|
175
|
+
const list = cookieList(p);
|
|
176
|
+
return onMain(() => new Promise<{ set: number }>((resolve) => {
|
|
177
|
+
const jar = WKWebsiteDataStore.defaultDataStore().httpCookieStore;
|
|
178
|
+
let left = list.length, set = 0;
|
|
179
|
+
const done = () => { if (--left <= 0) resolve({ set }); };
|
|
180
|
+
if (!left) return resolve({ set });
|
|
181
|
+
for (const c of list) {
|
|
182
|
+
const props = NSMutableDictionary.new<string, any>();
|
|
183
|
+
props.setObjectForKey(c.name, NSHTTPCookieName);
|
|
184
|
+
props.setObjectForKey(c.value ?? '', NSHTTPCookieValue);
|
|
185
|
+
props.setObjectForKey(c.domain, NSHTTPCookieDomain);
|
|
186
|
+
props.setObjectForKey(c.path || '/', NSHTTPCookiePath);
|
|
187
|
+
if (c.secure) props.setObjectForKey('TRUE', NSHTTPCookieSecure);
|
|
188
|
+
if (c.httpOnly) props.setObjectForKey('TRUE', 'HttpOnly'); // no public constant; the key CFNetwork reads
|
|
189
|
+
if (c.expires) props.setObjectForKey(NSDate.dateWithTimeIntervalSince1970(c.expires), NSHTTPCookieExpires);
|
|
190
|
+
if (c.sameSite === 'lax') props.setObjectForKey(NSHTTPCookieSameSiteLax, NSHTTPCookieSameSitePolicy);
|
|
191
|
+
else if (c.sameSite === 'strict') props.setObjectForKey(NSHTTPCookieSameSiteStrict, NSHTTPCookieSameSitePolicy);
|
|
192
|
+
const cookie = NSHTTPCookie.cookieWithProperties(props);
|
|
193
|
+
if (!cookie) { console.warn(`[webview] cookie skipped (invalid): ${c.name} @ ${c.domain}`); done(); continue; }
|
|
194
|
+
jar.setCookieCompletionHandler(cookie, () => { set++; done(); });
|
|
195
|
+
}
|
|
196
|
+
}));
|
|
197
|
+
});
|
|
163
198
|
}
|
|
164
199
|
|
|
165
200
|
function registerAndroid(): void {
|
|
@@ -302,4 +337,23 @@ function registerAndroid(): void {
|
|
|
302
337
|
return { open: !!wv };
|
|
303
338
|
}));
|
|
304
339
|
bridge.register('webview.close', () => onMain(() => { destroy(); }));
|
|
340
|
+
|
|
341
|
+
bridge.register('webview.cookies', (p: { cookies?: Cookie[] } | null) => {
|
|
342
|
+
const list = cookieList(p);
|
|
343
|
+
return onMain(() => new Promise<{ set: number }>((resolve) => {
|
|
344
|
+
const cm = android.webkit.CookieManager.getInstance();
|
|
345
|
+
let left = list.length, set = 0;
|
|
346
|
+
const done = () => { if (--left <= 0) { cm.flush(); resolve({ set }); } };
|
|
347
|
+
if (!left) return resolve({ set });
|
|
348
|
+
for (const c of list) {
|
|
349
|
+
const path = c.path || '/', host = c.domain.replace(/^\./, '');
|
|
350
|
+
const attrs = [`${c.name}=${c.value ?? ''}`, ...(c.domain.startsWith('.') ? [`Domain=${c.domain}`] : []), `Path=${path}`,
|
|
351
|
+
...(c.secure ? ['Secure'] : []), ...(c.httpOnly ? ['HttpOnly'] : []),
|
|
352
|
+
...(c.expires ? [`Expires=${new Date(c.expires * 1000).toUTCString()}`] : []),
|
|
353
|
+
...(c.sameSite ? [`SameSite=${c.sameSite}`] : [])];
|
|
354
|
+
cm.setCookie(`${c.secure ? 'https' : 'http'}://${host}${path}`, attrs.join('; '),
|
|
355
|
+
new android.webkit.ValueCallback<java.lang.Boolean>({ onReceiveValue: (ok) => { if (ok) set++; done(); } }));
|
|
356
|
+
}
|
|
357
|
+
}));
|
|
358
|
+
});
|
|
305
359
|
}
|
|
@@ -205,22 +205,33 @@ function armIosKeyboardObservers(): void {
|
|
|
205
205
|
armScrollClamp();
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
/** The page's colour where the keyboard meets it: the first opaque background up from the element at the
|
|
209
|
+
* bottom-centre (then body/html). A transparent body (rgba(0,0,0,0)) must not read as black. */
|
|
210
|
+
const BOTTOM_BG_JS = `(() => {
|
|
211
|
+
const op = (c) => c && !/^rgba\\(.*,\\s*0\\)$/.test(c) && c !== 'transparent';
|
|
212
|
+
for (let e = document.elementFromPoint(innerWidth / 2, innerHeight - 2); e; e = e.parentElement) { const c = getComputedStyle(e).backgroundColor; if (op(c)) return c; }
|
|
213
|
+
for (const e of [document.body, document.documentElement]) { const c = e && getComputedStyle(e).backgroundColor; if (op(c)) return c; }
|
|
214
|
+
return '';
|
|
215
|
+
})()`;
|
|
216
|
+
|
|
208
217
|
/**
|
|
209
|
-
* Sample the page's background color once per keyboard-show and paint the webview + its window with
|
|
218
|
+
* Sample the page's bottom background color once per keyboard-show and paint the webview + its window with
|
|
210
219
|
* it (Capacitor's autoBackdropColor) so there are no white flashes during the resize.
|
|
211
220
|
*/
|
|
212
221
|
function syncBackdropColor(): void {
|
|
213
222
|
try {
|
|
214
223
|
const wk = bridge.getWebView()?.ios as WKWebView | undefined;
|
|
215
224
|
if (!wk) return;
|
|
216
|
-
wk.evaluateJavaScriptCompletionHandler(
|
|
225
|
+
wk.evaluateJavaScriptCompletionHandler(BOTTOM_BG_JS, (result: any) => {
|
|
217
226
|
const m = /rgba?\(\s*(\d+)[,\s]+(\d+)[,\s]+(\d+)/.exec(String(result ?? ''));
|
|
218
227
|
if (!m) return;
|
|
219
228
|
Utils.dispatchToMainThread(() => {
|
|
220
229
|
const w = bridge.getWebView()?.ios as WKWebView | undefined;
|
|
221
230
|
if (!w) return;
|
|
222
231
|
const color = UIColor.colorWithRedGreenBlueAlpha(+m[1] / 255, +m[2] / 255, +m[3] / 255, 1);
|
|
223
|
-
|
|
232
|
+
// The keyboard's rounded corners (iOS 26) show whatever native view is behind the webview — paint
|
|
233
|
+
// the whole chain up to the window, not only the webview + window (a dark host view showed through).
|
|
234
|
+
for (let v: UIView | null = w; v; v = v.superview) v.backgroundColor = color;
|
|
224
235
|
if (w.window) w.window.backgroundColor = color;
|
|
225
236
|
if (SHELL_CONFIG.debug) appwrapNativeLog(`[native:keyboard] backdrop ← rgb(${m[1]},${m[2]},${m[3]})`);
|
|
226
237
|
});
|