@livx.cc/appwrap 0.51.2 → 0.52.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.
package/package.json
CHANGED
package/runtime/app/main-page.ts
CHANGED
|
@@ -17,6 +17,7 @@ import './shell/fcm-bootstrap.generated'; // side-effect: registers the FCM serv
|
|
|
17
17
|
import { startEventForwarding } from './shell/events';
|
|
18
18
|
import { startDevMenu } from './shell/devmenu';
|
|
19
19
|
import { showEnvBannerIfActive } from './shell/env-banner';
|
|
20
|
+
import { reassertEnvKeepAwake } from './shell/env-keepawake';
|
|
20
21
|
import { SHELL_CONFIG } from './shell/config';
|
|
21
22
|
import { bindStatusBarPage, setStatusBarStyle, applyThemeColor, enableAndroidEdgeToEdge, wireAndroidSafeArea } from './shell/status-bar';
|
|
22
23
|
import { CustomWebView } from './shell/custom-webview';
|
|
@@ -95,12 +96,6 @@ export function onPageLoaded(args: EventData): void {
|
|
|
95
96
|
if (initialized) return;
|
|
96
97
|
initialized = true;
|
|
97
98
|
|
|
98
|
-
// Debug mode: keep the screen awake (no auto-lock while foreground) so the dev/inspect session
|
|
99
|
-
// and the iterate loop stay alive. iOS via the idle timer; Android via WebView keepScreenOn.
|
|
100
|
-
if (isIOS && SHELL_CONFIG.debug) {
|
|
101
|
-
try { UIApplication.sharedApplication.idleTimerDisabled = true; } catch (e) { /* no-op */ }
|
|
102
|
-
}
|
|
103
|
-
|
|
104
99
|
registerHandlers();
|
|
105
100
|
registerExtendedHandlers();
|
|
106
101
|
registerParityHandlers();
|
|
@@ -138,6 +133,9 @@ export function onPageLoaded(args: EventData): void {
|
|
|
138
133
|
// Env indicator banner: shown in the bottom safe area on relaunch when a non-default env override is
|
|
139
134
|
// active (env-switcher only). Bottom-safe-area, auto-shrinks to a pill after 3s. No-op otherwise.
|
|
140
135
|
showEnvBannerIfActive();
|
|
136
|
+
// Keep the screen awake while pointed at a NON-DEFAULT backend (or in a debug build) — same signal as the
|
|
137
|
+
// banner above, so "banner showing" ⟺ "no auto-lock". Subsumes the old iOS-only debug idle-timer block.
|
|
138
|
+
reassertEnvKeepAwake();
|
|
141
139
|
|
|
142
140
|
// Halt the WebView render + JS-timer pipeline while backgrounded so a page running a continuous
|
|
143
141
|
// animation (Android doesn't auto-pause rAF off-screen) stops burning CPU/battery. No-op on iOS.
|
|
@@ -147,6 +145,10 @@ export function onPageLoaded(args: EventData): void {
|
|
|
147
145
|
// iOS: wake the WebContent renderer NOW (it stays THROTTLED for ~30-45s after a full-window system
|
|
148
146
|
// surface — StoreKit manage-subscriptions sheet, itms-apps deep link, backgrounding — froze it).
|
|
149
147
|
webView.wakeWebContent();
|
|
148
|
+
// Neither platform guarantees the wake lock survives a background trip (Android drops the window flag
|
|
149
|
+
// outright if the Activity was recreated). Assert-only: never write `false`, so an app driving the
|
|
150
|
+
// public `ui.keepAwake` bridge API itself (e.g. during video) isn't clobbered on resume.
|
|
151
|
+
reassertEnvKeepAwake();
|
|
150
152
|
});
|
|
151
153
|
|
|
152
154
|
// DEBUG: trace the iOS app-lifecycle event sequence to the pullable log sink so we can see WHICH
|
|
@@ -420,6 +420,40 @@ function createAssetServingClient(): android.webkit.WebViewClient {
|
|
|
420
420
|
view.evaluateJavascript(buildBootstrapJs(), null as unknown as android.webkit.ValueCallback<string>);
|
|
421
421
|
};
|
|
422
422
|
|
|
423
|
+
// Main-frame load FAILURE → NativeScript's `loadFinished` event (its `error` arg). We replace NS's own
|
|
424
|
+
// WebViewClientImpl wholesale (see initNativeView), and NS only raises that event from ITS client — so
|
|
425
|
+
// without this forward the event NEVER fires on Android and a failed load (server down, DNS, refused)
|
|
426
|
+
// is invisible to JS. iOS keeps the event because DevCertNavDelegate forwards didFail*Navigation to
|
|
427
|
+
// NS's delegate; this restores the SAME seam here rather than adding a parallel Android-only one
|
|
428
|
+
// (consumer: env-switcher's reloadToEffective).
|
|
429
|
+
// MAIN FRAME ONLY: API 23+ also reports sub-resource failures here, and a dead favicon/image must not
|
|
430
|
+
// be mistaken for "the page didn't load". minSdk is 26, so only this modern overload is ever dispatched
|
|
431
|
+
// (the legacy string-arg overload is API < 23).
|
|
432
|
+
const receivedError = (
|
|
433
|
+
view: android.webkit.WebView,
|
|
434
|
+
request: android.webkit.WebResourceRequest,
|
|
435
|
+
error: android.webkit.WebResourceError
|
|
436
|
+
): void => {
|
|
437
|
+
if (!request?.isForMainFrame?.()) return;
|
|
438
|
+
// `_onLoadFinished` is NS's internal event-raiser (the `_` API index.android.js itself calls) — not
|
|
439
|
+
// in the public typings, hence the cast.
|
|
440
|
+
(CustomWebView.forNative(view) as any)?._onLoadFinished(
|
|
441
|
+
String(request.getUrl?.() ?? ''),
|
|
442
|
+
`${error?.getDescription?.() ?? 'Load failed'} (${error?.getErrorCode?.() ?? '?'})`
|
|
443
|
+
);
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
// Main-frame load SUCCESS → NativeScript's `loadFinished` event with NO `error` arg. The counterpart of
|
|
447
|
+
// `receivedError` above, and NOT optional: NS raises this from ITS WebViewClientImpl.onPageFinished,
|
|
448
|
+
// which we replace wholesale — so forwarding only the FAILURE half would leave `loadFinished` never
|
|
449
|
+
// firing on a SUCCESSFUL load. A one-shot `loadFinished` listener (env-switcher's reloadToEffective)
|
|
450
|
+
// would then never unhook on success and would leak onto the NEXT navigation, blaming a later error on
|
|
451
|
+
// an earlier switch (and naming the earlier switch's host). Both halves or neither.
|
|
452
|
+
const pageFinished = (view: android.webkit.WebView, url: string): void => {
|
|
453
|
+
// Mirrors NS's own `owner._onLoadFinished(url, undefined)` — absent `error` is what marks success.
|
|
454
|
+
(CustomWebView.forNative(view) as any)?._onLoadFinished(String(url ?? ''));
|
|
455
|
+
};
|
|
456
|
+
|
|
423
457
|
// DEBUG-ONLY dev-server cert trust (Android parity with the iOS WKNavigationDelegate). `appwrap dev`
|
|
424
458
|
// points at a LAN dev server that almost always uses a self-signed / mkcert TLS cert the device's
|
|
425
459
|
// trust store doesn't know — the WebView would otherwise hard-fail with ERR_CERT_AUTHORITY_INVALID.
|
|
@@ -468,9 +502,15 @@ function createAssetServingClient(): android.webkit.WebViewClient {
|
|
|
468
502
|
onPageStarted(view: android.webkit.WebView, _url: string, _favicon: android.graphics.Bitmap): void {
|
|
469
503
|
pageStarted(view);
|
|
470
504
|
},
|
|
505
|
+
onPageFinished(view: android.webkit.WebView, url: string): void {
|
|
506
|
+
pageFinished(view, url);
|
|
507
|
+
},
|
|
471
508
|
onReceivedSslError(_view: android.webkit.WebView, handler: android.webkit.SslErrorHandler, error: android.net.http.SslError): void {
|
|
472
509
|
receivedSslError(handler, error);
|
|
473
510
|
},
|
|
511
|
+
onReceivedError(view: android.webkit.WebView, request: android.webkit.WebResourceRequest, error: android.webkit.WebResourceError): void {
|
|
512
|
+
receivedError(view, request, error);
|
|
513
|
+
},
|
|
474
514
|
})
|
|
475
515
|
: (android.webkit.WebViewClient as any).extend({
|
|
476
516
|
shouldInterceptRequest(_view: android.webkit.WebView, request: android.webkit.WebResourceRequest | string): android.webkit.WebResourceResponse | null {
|
|
@@ -482,9 +522,15 @@ function createAssetServingClient(): android.webkit.WebViewClient {
|
|
|
482
522
|
onPageStarted(view: android.webkit.WebView, _url: string, _favicon: android.graphics.Bitmap): void {
|
|
483
523
|
pageStarted(view);
|
|
484
524
|
},
|
|
525
|
+
onPageFinished(view: android.webkit.WebView, url: string): void {
|
|
526
|
+
pageFinished(view, url);
|
|
527
|
+
},
|
|
485
528
|
onReceivedSslError(_view: android.webkit.WebView, handler: android.webkit.SslErrorHandler, error: android.net.http.SslError): void {
|
|
486
529
|
receivedSslError(handler, error);
|
|
487
530
|
},
|
|
531
|
+
onReceivedError(view: android.webkit.WebView, request: android.webkit.WebResourceRequest, error: android.webkit.WebResourceError): void {
|
|
532
|
+
receivedError(view, request, error);
|
|
533
|
+
},
|
|
488
534
|
});
|
|
489
535
|
return new assetClientClass();
|
|
490
536
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Application, Utils, isAndroid, isIOS } from '@nativescript/core';
|
|
2
|
+
import { SHELL_CONFIG } from './config';
|
|
3
|
+
import { isNonDefaultOverride } from './env-switcher';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Keep the screen awake while the shell is pointed at a NON-PRODUCTION backend, so a long on-device test
|
|
7
|
+
* session isn't killed by auto-lock. Companion to `env-banner.ts`: same trigger, same reconcile points.
|
|
8
|
+
*
|
|
9
|
+
* WHY THE BACKEND, NOT THE BUILD: `SHELL_CONFIG.debug` is a BUILD-TIME fact, and it misses the case that
|
|
10
|
+
* actually matters — a RELEASE/TestFlight binary (`debug:false`) whose env-switcher is pointed at lab or a
|
|
11
|
+
* local dev server is a test session in every way that counts, and it auto-locks mid-test. Conversely the
|
|
12
|
+
* signal must never fire for a real user. `isNonDefaultOverride()` is exactly that line: it is true only
|
|
13
|
+
* when a persisted `kit:serverUrlOverride` resolves to a host DIFFERENT from the build-time default
|
|
14
|
+
* (`SHELL_CONFIG.serverUrl`). A real user never sets an override, so this cannot leak into production —
|
|
15
|
+
* and it is the SAME predicate the amber env banner keys off, so "banner visible" ⟺ "screen stays awake",
|
|
16
|
+
* by construction rather than by two rules kept in sync by hand.
|
|
17
|
+
*
|
|
18
|
+
* `debug` is retained as an OR, not a replacement: a debug build has its own reason to stay awake (the
|
|
19
|
+
* inspect/iterate loop) even on the default env. This subsumes the iOS-only `idleTimerDisabled` block that
|
|
20
|
+
* used to sit inline in main-page.ts; the debug-gated Android half still lives in `custom-webview.android.ts`
|
|
21
|
+
* (`wv.setKeepScreenOn(true)`), which is harmlessly redundant with this module's Android path in a debug
|
|
22
|
+
* build — both are idempotent "keep on" assertions, and only this one also covers the env-override case.
|
|
23
|
+
*
|
|
24
|
+
* Inert for `loader !== 'server'` / no envSwitcher config: `isNonDefaultOverride()` returns false via
|
|
25
|
+
* `isEnvSwitcherEnabled()`, so a non-switcher app reduces to the plain `debug` behaviour (no crash).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** Desired wake-lock state: a non-default backend override (the banner's signal), or a debug build. */
|
|
29
|
+
export function shouldKeepAwake(): boolean {
|
|
30
|
+
return !!SHELL_CONFIG.debug || isNonDefaultOverride();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let androidRetries = 0; // bounds the boot "activity not ready" retry so it can't spin forever
|
|
34
|
+
|
|
35
|
+
/** Apply the wake lock natively. iOS: the app-wide idle timer. Android: the window's KEEP_SCREEN_ON flag.
|
|
36
|
+
*
|
|
37
|
+
* The Android boot retry mirrors `env-banner.ts`: at `onPageLoaded` on a relaunch the Activity is not
|
|
38
|
+
* necessarily attached yet, so `foregroundActivity` can be null. Returning silently there would drop the
|
|
39
|
+
* wake lock in exactly the common case (relaunch straight into lab) — so retry, bounded (~2s), and
|
|
40
|
+
* re-read the DESIRED state at each attempt so a switch/reset landing mid-window wins over a stale one. */
|
|
41
|
+
function applyKeepAwake(on: boolean): void {
|
|
42
|
+
if (isIOS) {
|
|
43
|
+
Utils.dispatchToMainThread(() => {
|
|
44
|
+
try { UIApplication.sharedApplication.idleTimerDisabled = on; } catch (e) { /* no-op */ }
|
|
45
|
+
});
|
|
46
|
+
} else if (isAndroid) {
|
|
47
|
+
const activity = Application.android?.foregroundActivity || Application.android?.startActivity;
|
|
48
|
+
if (!activity) {
|
|
49
|
+
if (androidRetries++ < 20) setTimeout(() => applyKeepAwake(shouldKeepAwake()), 100);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
androidRetries = 0;
|
|
53
|
+
activity.runOnUiThread(new java.lang.Runnable({
|
|
54
|
+
run() {
|
|
55
|
+
try {
|
|
56
|
+
const window = activity.getWindow();
|
|
57
|
+
if (!window) return;
|
|
58
|
+
const flag = android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
|
|
59
|
+
on ? window.addFlags(flag) : window.clearFlags(flag);
|
|
60
|
+
} catch (e) { /* no-op */ }
|
|
61
|
+
},
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Reconcile the wake lock to the EXACT desired state — including turning it OFF. For explicit env
|
|
68
|
+
* transitions only (`applySwitch`, alongside `refreshEnvBanner`), where clearing on a reset-to-default is
|
|
69
|
+
* the whole point. Idempotent.
|
|
70
|
+
*/
|
|
71
|
+
export function refreshEnvKeepAwake(): void {
|
|
72
|
+
applyKeepAwake(shouldKeepAwake());
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Assert the wake lock if this env wants it — but NEVER write `false`. For the boot and resume paths.
|
|
77
|
+
*
|
|
78
|
+
* WHY ASSERT-ONLY, not a full reconcile: the hosted app may hold the screen on ITSELF — either via the
|
|
79
|
+
* PUBLIC `ui.keepAwake` bridge API (handlers-extended / handlers-android), or via the web Screen Wake Lock
|
|
80
|
+
* API (`navigator.wakeLock`), which Android WebView honours by keeping the same KEEP_SCREEN_ON state. That
|
|
81
|
+
* is not hypothetical: the AGF app holds a `navigator.wakeLock` for the duration of a live room, and the
|
|
82
|
+
* flag is observable on its window (`dumpsys window` → `fl=KEEP_SCREEN_ON`) with no override set at all.
|
|
83
|
+
* A full reconcile on resume would write `false` on the default env and silently CLOBBER that app's own
|
|
84
|
+
* lock after any background trip. Writing `false` here would also buy nothing: a fresh process
|
|
85
|
+
* starts with the idle timer enabled and a fresh Activity window has no KEEP_SCREEN_ON flag, so "off" is
|
|
86
|
+
* already the state at boot. Only the ON direction needs re-asserting.
|
|
87
|
+
*
|
|
88
|
+
* ON DOES need re-asserting: neither platform guarantees the lock survives a background trip. Android
|
|
89
|
+
* drops the window flag outright if the Activity is recreated (config change / process-death restore) —
|
|
90
|
+
* a NEW window has no flag, and `initialized` in main-page.ts would not re-run the boot path.
|
|
91
|
+
*/
|
|
92
|
+
export function reassertEnvKeepAwake(): void {
|
|
93
|
+
if (shouldKeepAwake()) applyKeepAwake(true);
|
|
94
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { ApplicationSettings, Dialogs, Utils, isAndroid, isIOS } from '@nativescript/core';
|
|
2
|
+
import type { EventData, LoadEventData } from '@nativescript/core';
|
|
2
3
|
import { SHELL_CONFIG } from './config';
|
|
3
4
|
import { OVERRIDE_KEY, effectiveServerUrl, isUrlAllowed } from './server-url';
|
|
4
5
|
import { bridge } from './bridge';
|
|
5
6
|
import { refreshEnvBanner } from './env-banner';
|
|
7
|
+
import { refreshEnvKeepAwake } from './env-keepawake';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Runtime env-switcher — re-point a `loader:'server'` shell between declared environments (prod / lab /
|
|
@@ -72,12 +74,51 @@ function clearOverride(): void {
|
|
|
72
74
|
ApplicationSettings.remove(OVERRIDE_KEY);
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
/**
|
|
78
|
+
* The switch is PERSISTED but the visible page did NOT change — the exact gap the confirm prompt's "The
|
|
79
|
+
* app will reload" leaves the user staring at. Silence here reads as "the switch didn't work" (it did:
|
|
80
|
+
* the override is already written, so a cold start WILL land on it), which is the actionable half and the
|
|
81
|
+
* half that was missing. Name the host and the reason so an unreachable target (dev server down, DNS,
|
|
82
|
+
* TLS/ATS refusal) is distinguishable from a broken switcher.
|
|
83
|
+
*/
|
|
84
|
+
function notifyNotReloaded(url: string, reason: string): void {
|
|
85
|
+
console.warn(`AppWrap: env switch persisted but the in-session reload did not happen (${hostOf(url)}): ${reason}`);
|
|
86
|
+
void Dialogs.alert({
|
|
87
|
+
title: 'Still on the old environment',
|
|
88
|
+
message: `Couldn't load ${hostOf(url)}.\n${reason}\n\nThe environment change IS saved — relaunch the app to use it.`,
|
|
89
|
+
okButtonText: 'OK',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
75
93
|
/** Load the current effective server URL into the live WebView (immediate switch — no wait for a cold
|
|
76
|
-
* start; the persisted override also makes it stick across relaunch via the boot loader).
|
|
94
|
+
* start; the persisted override also makes it stick across relaunch via the boot loader).
|
|
95
|
+
*
|
|
96
|
+
* NEVER fail silently: both the no-WebView path and a FAILED load report through `notifyNotReloaded`.
|
|
97
|
+
* The failure seam is NativeScript's own `loadFinished` event, whose `error` arg both platforms already
|
|
98
|
+
* populate for a main-frame failure (iOS: WKNavigationDelegate didFail[Provisional]Navigation — a refused
|
|
99
|
+
* connection is PROVISIONAL, which is exactly the case that bit; Android: WebViewClient.onReceivedError,
|
|
100
|
+
* re-wired to NS in custom-webview.android.ts because our client replaces NS's). Reusing it keeps ONE
|
|
101
|
+
* cross-platform seam — do NOT install a second WKNavigationDelegate here: custom-webview.ios.ts already
|
|
102
|
+
* owns one (DevCertNavDelegate) and a competing delegate would silently unhook its cert trust.
|
|
103
|
+
*
|
|
104
|
+
* The handler is ONE-SHOT and armed immediately before the load, so it observes this switch's outcome and
|
|
105
|
+
* cannot leak or mis-attribute a later navigation's error to the switch.
|
|
106
|
+
*/
|
|
77
107
|
function reloadToEffective(): void {
|
|
78
|
-
const wv = bridge.getWebView();
|
|
79
|
-
if (!wv) return;
|
|
80
108
|
const url = effectiveServerUrl();
|
|
109
|
+
const wv = bridge.getWebView();
|
|
110
|
+
if (!wv) {
|
|
111
|
+
notifyNotReloaded(url, 'The app view is not available.');
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
// `on` resolves to Observable's generic overload here (CustomWebView widens the WebView-specific one),
|
|
115
|
+
// so take EventData and narrow — `loadFinished` always carries LoadEventData.
|
|
116
|
+
const onLoadFinished = (args: EventData) => {
|
|
117
|
+
wv.off('loadFinished', onLoadFinished);
|
|
118
|
+
const error = (args as LoadEventData).error;
|
|
119
|
+
if (error) notifyNotReloaded(url, String(error));
|
|
120
|
+
};
|
|
121
|
+
wv.on('loadFinished', onLoadFinished);
|
|
81
122
|
Utils.dispatchToMainThread(() => {
|
|
82
123
|
if (isIOS && wv.ios) {
|
|
83
124
|
(wv.ios as WKWebView).loadRequest(NSURLRequest.requestWithURL(NSURL.URLWithString(url)));
|
|
@@ -104,6 +145,7 @@ async function applySwitch(url: string | null, label: string): Promise<void> {
|
|
|
104
145
|
else clearOverride();
|
|
105
146
|
reloadToEffective();
|
|
106
147
|
refreshEnvBanner(); // in-session: reflect the new env (switch) or hide (reset) — not just on relaunch
|
|
148
|
+
refreshEnvKeepAwake(); // same signal as the banner: awake off-default, normal on a reset — never disagree
|
|
107
149
|
}
|
|
108
150
|
|
|
109
151
|
let menuOpen = false;
|
|
@@ -236,14 +278,22 @@ export async function handleEnvDeepLink(link: string): Promise<void> {
|
|
|
236
278
|
}
|
|
237
279
|
}
|
|
238
280
|
|
|
239
|
-
/** Free-form URL entry, gated by `allowPattern` (default-deny). Rejects a non-matching URL.
|
|
281
|
+
/** Free-form URL entry, gated by `allowPattern` (default-deny). Rejects a non-matching URL.
|
|
282
|
+
*
|
|
283
|
+
* The field is deliberately EMPTY — do NOT re-add a `defaultText: 'https://'` prefill. A URL is the one
|
|
284
|
+
* input class users always PASTE, and a copied URL already carries its own scheme, so a prefill breaks
|
|
285
|
+
* exactly the case this field exists for: iOS pastes at the caret (which sits AFTER the prefill), giving
|
|
286
|
+
* `https://https://host` → rejected by `allowPattern` as "Not allowed". Typing masks the bug (you type the
|
|
287
|
+
* host after the prefix), which is why it reads as "paste doesn't work". An empty field is also where iOS
|
|
288
|
+
* offers Paste most readily — long-pressing a NON-empty field raises the cursor loupe, not the edit menu.
|
|
289
|
+
* The `https://` hint lives in `message` instead, where it can't contaminate the value.
|
|
290
|
+
*/
|
|
240
291
|
async function promptOther(): Promise<void> {
|
|
241
292
|
const res = await Dialogs.prompt({
|
|
242
293
|
title: 'Custom environment',
|
|
243
|
-
message: 'Enter an allowed URL (https://…).',
|
|
294
|
+
message: 'Enter or paste an allowed URL (https://…).',
|
|
244
295
|
okButtonText: 'Next',
|
|
245
296
|
cancelButtonText: 'Cancel',
|
|
246
|
-
defaultText: 'https://',
|
|
247
297
|
inputType: 'text',
|
|
248
298
|
});
|
|
249
299
|
if (!res?.result || !res.text) return;
|