@livx.cc/appwrap 0.26.0 → 0.28.1
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 +1 -1
- package/runtime/App_Resources/Android/src/main/AndroidManifest.xml +3 -0
- package/runtime/app/app.ts +57 -39
- package/runtime/app/shell/android-helpers.ts +6 -6
- package/runtime/app/shell/background-bootstrap.generated.ts +8 -0
- package/runtime/app/shell/background-context.ts +20 -0
- package/runtime/app/shell/banner.ts +9 -4
- package/runtime/app/shell/bridge.ts +2 -0
- package/runtime/app/shell/capabilities.manifest.ts +22 -3
- package/runtime/app/shell/custom-webview.android.ts +50 -23
- package/runtime/app/shell/custom-webview.ios.ts +90 -78
- package/runtime/app/shell/devmenu.ts +2 -2
- package/runtime/app/shell/env.ts +1 -3
- package/runtime/app/shell/events.ts +25 -4
- package/runtime/app/shell/fcm-firebase.d.ts +2 -1
- package/runtime/app/shell/handlers-android.ts +16 -12
- package/runtime/app/shell/handlers-background.ts +290 -0
- package/runtime/app/shell/handlers-billing.ts +90 -82
- package/runtime/app/shell/handlers-extended.ts +141 -110
- package/runtime/app/shell/handlers-fs.ts +53 -41
- package/runtime/app/shell/handlers-health.ts +27 -19
- package/runtime/app/shell/handlers-media.ts +8 -14
- package/runtime/app/shell/handlers-oauth.ts +19 -19
- package/runtime/app/shell/handlers-parity.ts +118 -89
- package/runtime/app/shell/handlers-push.ts +5 -15
- package/runtime/app/shell/handlers-reviews.ts +5 -5
- package/runtime/app/shell/handlers-scanner.ts +36 -38
- package/runtime/app/shell/handlers-speech.ts +43 -49
- package/runtime/app/shell/handlers-system.ts +122 -1
- package/runtime/app/shell/handlers.ts +10 -3
- package/runtime/app/shell/ios-frameworks.d.ts +9 -44
- package/runtime/app/shell/ios-ui-delegate.ts +112 -108
- package/runtime/app/shell/native-log.ts +1 -3
- package/runtime/app/shell/status-bar.ts +7 -4
- package/runtime/app/shell/toast.ts +1 -1
- package/runtime/references.d.ts +21 -0
- package/runtime/tsconfig.json +1 -0
- package/src/cli.ts +74 -18
- package/src/config.ts +20 -0
- package/src/derive.ts +75 -0
package/package.json
CHANGED
package/runtime/app/app.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Application, isIOS } from '@nativescript/core';
|
|
2
|
-
import { onDeepLink } from './shell/events';
|
|
2
|
+
import { onDeepLink, onShortcut } from './shell/events';
|
|
3
3
|
import { iosOrientationMask } from './shell/orientation';
|
|
4
4
|
import { installForegroundNotificationDelegate } from './shell/handlers-extended';
|
|
5
5
|
import { onApnsToken, onApnsError, onRemoteMessage } from './shell/handlers-push';
|
|
6
|
+
import { registerBackgroundLaunchHandlers } from './shell/background-bootstrap.generated';
|
|
6
7
|
|
|
7
8
|
/** APNs device token (NSData) → lowercase hex string the push backend expects. */
|
|
8
9
|
function apnsTokenToHex(deviceToken: NSData): string {
|
|
@@ -15,44 +16,61 @@ function apnsTokenToHex(deviceToken: NSData): string {
|
|
|
15
16
|
if (isIOS) {
|
|
16
17
|
// Custom delegate for URL-scheme opens. NativeScript tracks lifecycle via
|
|
17
18
|
// UIApplication notifications, so adding a delegate only extends behavior.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
19
|
+
@NativeClass()
|
|
20
|
+
class AppWrapAppDelegate extends UIResponder implements UIApplicationDelegate {
|
|
21
|
+
static ObjCProtocols = [UIApplicationDelegate];
|
|
22
|
+
|
|
23
|
+
applicationDidFinishLaunchingWithOptions(_app: UIApplication, launchOptions: NSDictionary<string, any>): boolean {
|
|
24
|
+
// Set the notification delegate NOW so a cold-launch tap's deep link is delivered.
|
|
25
|
+
installForegroundNotificationDelegate();
|
|
26
|
+
// BGTaskScheduler launch handlers MUST be registered here (Apple's rule) — no-op unless the
|
|
27
|
+
// backgroundTask module is active (the CLI wires the generated bootstrap to the real impl).
|
|
28
|
+
registerBackgroundLaunchHandlers();
|
|
29
|
+
const url = launchOptions?.objectForKey(UIApplicationLaunchOptionsURLKey) as NSURL | null;
|
|
30
|
+
if (url) onDeepLink(url.absoluteString ?? String(url));
|
|
31
|
+
// Cold-launch from a home-screen quick action — Apple delivers it ONLY here in launchOptions
|
|
32
|
+
// (performActionForShortcutItem is NOT called for the launch action). Buffered until handshake.
|
|
33
|
+
const shortcut = launchOptions?.objectForKey(UIApplicationLaunchOptionsShortcutItemKey) as UIApplicationShortcutItem | null;
|
|
34
|
+
if (shortcut) onShortcut(String(shortcut.type));
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
// Warm activation of a quick action (app already running/backgrounded).
|
|
38
|
+
applicationPerformActionForShortcutItemCompletionHandler(
|
|
39
|
+
_app: UIApplication,
|
|
40
|
+
shortcutItem: UIApplicationShortcutItem,
|
|
41
|
+
completionHandler: (done: boolean) => void
|
|
42
|
+
): void {
|
|
43
|
+
onShortcut(String(shortcutItem.type));
|
|
44
|
+
completionHandler(true);
|
|
45
|
+
}
|
|
46
|
+
applicationOpenURLOptions(_app: UIApplication, url: NSURL, _options: NSDictionary<string, any>): boolean {
|
|
47
|
+
console.log('AppWrap: openURL', url.absoluteString);
|
|
48
|
+
onDeepLink(url.absoluteString);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
// Drives `kit.screen.orientation.lock` — UIKit reads this on every rotation.
|
|
52
|
+
applicationSupportedInterfaceOrientationsForWindow(_app: UIApplication, _window: UIWindow): UIInterfaceOrientationMask {
|
|
53
|
+
// iosOrientationMask() returns the raw bitmask as a number; the protocol types it as the enum.
|
|
54
|
+
return iosOrientationMask() as UIInterfaceOrientationMask;
|
|
55
|
+
}
|
|
56
|
+
// ── Remote push (APNs) — delivered to the AppDelegate, bridged to kit.push. ──
|
|
57
|
+
applicationDidRegisterForRemoteNotificationsWithDeviceToken(_app: UIApplication, deviceToken: NSData): void {
|
|
58
|
+
onApnsToken(apnsTokenToHex(deviceToken));
|
|
59
|
+
}
|
|
60
|
+
applicationDidFailToRegisterForRemoteNotificationsWithError(_app: UIApplication, error: NSError): void {
|
|
61
|
+
onApnsError(error?.localizedDescription ?? 'APNs registration failed');
|
|
62
|
+
}
|
|
63
|
+
// Background/silent + content-available pushes (a tapped alert routes via the UN delegate).
|
|
64
|
+
applicationDidReceiveRemoteNotificationFetchCompletionHandler(
|
|
65
|
+
_app: UIApplication,
|
|
66
|
+
userInfo: NSDictionary<any, any>,
|
|
67
|
+
completionHandler: (result: UIBackgroundFetchResult) => void
|
|
68
|
+
): void {
|
|
69
|
+
onRemoteMessage(userInfo, false);
|
|
70
|
+
completionHandler(UIBackgroundFetchResult.NewData);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
Application.ios.delegate = AppWrapAppDelegate;
|
|
56
74
|
}
|
|
57
75
|
|
|
58
76
|
Application.run({ moduleName: 'main-page' });
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { Application, Utils } from '@nativescript/core';
|
|
2
2
|
|
|
3
|
-
declare const android: any, java: any;
|
|
4
|
-
|
|
5
3
|
let nextCode = 7300;
|
|
6
4
|
|
|
7
|
-
function foregroundActivity()
|
|
5
|
+
function foregroundActivity() {
|
|
8
6
|
return Application.android.foregroundActivity ?? Application.android.startActivity;
|
|
9
7
|
}
|
|
10
8
|
|
|
@@ -20,6 +18,7 @@ export function requestPermissions(permissions: string[]): Promise<boolean> {
|
|
|
20
18
|
|
|
21
19
|
const code = nextCode++;
|
|
22
20
|
return new Promise((resolve) => {
|
|
21
|
+
// interop: NS activity-event payload is dispatched untyped via Application.android.on
|
|
23
22
|
const onResult = (args: any) => {
|
|
24
23
|
if (args.requestCode !== code) return;
|
|
25
24
|
Application.android.off(Application.android.activityRequestPermissionsEvent as any, onResult);
|
|
@@ -32,7 +31,7 @@ export function requestPermissions(permissions: string[]): Promise<boolean> {
|
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
/** Decode a content Uri (downscaled to `maxSize` longest edge) into a JPEG data URL. */
|
|
35
|
-
export function uriToDataUrl(uri:
|
|
34
|
+
export function uriToDataUrl(uri: android.net.Uri, maxSize: number): string | null {
|
|
36
35
|
const cr = Utils.android.getApplicationContext().getContentResolver();
|
|
37
36
|
const bounds = new android.graphics.BitmapFactory.Options();
|
|
38
37
|
bounds.inJustDecodeBounds = true;
|
|
@@ -51,7 +50,7 @@ export function uriToDataUrl(uri: any, maxSize: number): string | null {
|
|
|
51
50
|
}
|
|
52
51
|
|
|
53
52
|
/** Compress an Android Bitmap to a JPEG data URL. */
|
|
54
|
-
export function bitmapToDataUrl(bmp:
|
|
53
|
+
export function bitmapToDataUrl(bmp: android.graphics.Bitmap): string {
|
|
55
54
|
const baos = new java.io.ByteArrayOutputStream();
|
|
56
55
|
bmp.compress(android.graphics.Bitmap.CompressFormat.JPEG, 85, baos);
|
|
57
56
|
const b64 = android.util.Base64.encodeToString(baos.toByteArray(), android.util.Base64.NO_WRAP);
|
|
@@ -59,12 +58,13 @@ export function bitmapToDataUrl(bmp: any): string {
|
|
|
59
58
|
}
|
|
60
59
|
|
|
61
60
|
/** Launch an intent and resolve with its activity result. */
|
|
62
|
-
export function startActivityForResult(intent:
|
|
61
|
+
export function startActivityForResult(intent: android.content.Intent): Promise<{ resultCode: number; intent: android.content.Intent }> {
|
|
63
62
|
const activity = foregroundActivity();
|
|
64
63
|
if (!activity) return Promise.reject(Object.assign(new Error('no foreground activity'), { code: 'NOT_READY' }));
|
|
65
64
|
|
|
66
65
|
const code = nextCode++;
|
|
67
66
|
return new Promise((resolve) => {
|
|
67
|
+
// interop: NS activity-event payload is dispatched untyped via Application.android.on
|
|
68
68
|
const onResult = (args: any) => {
|
|
69
69
|
if (args.requestCode !== code) return;
|
|
70
70
|
Application.android.off(Application.android.activityResultEvent as any, onResult);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated by `appwrap init`/`sync`. iOS BGTaskScheduler launch handlers MUST be registered at
|
|
3
|
+
* `applicationDidFinishLaunchingWithOptions` (Apple's rule) — too early for the page-load handler
|
|
4
|
+
* barrel. The AppDelegate calls {@link registerBackgroundLaunchHandlers} from didFinishLaunching; the
|
|
5
|
+
* CLI rewrites this to import the real implementation ONLY when the `backgroundTask` module is active
|
|
6
|
+
* (so a build without it never references BGTaskScheduler). Default = no-op. Do not edit.
|
|
7
|
+
*/
|
|
8
|
+
export function registerBackgroundLaunchHandlers(): void {}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background-launch context seam — a tiny always-bundled module so the (always-on) `app.handshake`
|
|
3
|
+
* handler can report a wake id WITHOUT importing the strippable `handlers-background` file (which only
|
|
4
|
+
* lands when the `backgroundTask` module is active). The headless runner sets the id before loading
|
|
5
|
+
* the offscreen WebView; the handshake reads it ONCE (so a foreground handshake later in the same
|
|
6
|
+
* process never re-reports a stale wake). No NativeScript globals → also unit-testable.
|
|
7
|
+
*/
|
|
8
|
+
let pendingBackgroundTaskId: string | null = null;
|
|
9
|
+
|
|
10
|
+
/** Set by the headless background runner before it loads the offscreen WebView for `id`. */
|
|
11
|
+
export function setPendingBackgroundTaskId(id: string | null): void {
|
|
12
|
+
pendingBackgroundTaskId = id;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Read-and-clear the pending wake id (called by the handshake handler). */
|
|
16
|
+
export function consumePendingBackgroundTaskId(): string | null {
|
|
17
|
+
const id = pendingBackgroundTaskId;
|
|
18
|
+
pendingBackgroundTaskId = null;
|
|
19
|
+
return id;
|
|
20
|
+
}
|
|
@@ -9,7 +9,7 @@ import { bridge } from './bridge';
|
|
|
9
9
|
|
|
10
10
|
let currentId: string | null = null;
|
|
11
11
|
let iosBanner: UIView | null = null;
|
|
12
|
-
let iosTapHandler: any = null;
|
|
12
|
+
let iosTapHandler: any = null; // runtime-built ObjC gesture target (see iosGestureHandlerClass)
|
|
13
13
|
let androidBanner: android.view.View | null = null;
|
|
14
14
|
|
|
15
15
|
function onTap(): void {
|
|
@@ -41,8 +41,13 @@ export function dismissBanner(): void {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
// ── iOS ──────────────────────────────────────────────────────────────
|
|
44
|
-
//
|
|
45
|
-
//
|
|
44
|
+
// Target/action handler for the banner's tap + swipe-down gestures (bare selectors exposed via
|
|
45
|
+
// `exposedMethods`, no ObjC protocol). Built lazily INSIDE this iOS-only path — `interop` and
|
|
46
|
+
// `NSObject` don't exist on Android, and this file is imported on both platforms via handlers.ts, so
|
|
47
|
+
// a top-level `@NativeClass` with a `static ObjCExposedMethods = { … interop.types.void }` initializer
|
|
48
|
+
// would touch `interop` at module load and crash the Android shell (same reason handlers-scanner keeps
|
|
49
|
+
// its `exposedMethods` cancel-button target as a runtime `.extend` inside its iOS-only registrar).
|
|
50
|
+
// any — runtime-built ObjC subclass; an exposedMethods (non-protocol) target has no static type.
|
|
46
51
|
let IOSGestureHandler: any = null;
|
|
47
52
|
function iosGestureHandlerClass(): any {
|
|
48
53
|
if (!IOSGestureHandler) {
|
|
@@ -117,7 +122,7 @@ function showAndroidBanner(message: string): void {
|
|
|
117
122
|
const SWIPE = 40 * density;
|
|
118
123
|
let downX = 0, downY = 0, downT = 0;
|
|
119
124
|
tv.setOnTouchListener(new android.view.View.OnTouchListener({
|
|
120
|
-
onTouch(_v:
|
|
125
|
+
onTouch(_v: android.view.View, e: android.view.MotionEvent) {
|
|
121
126
|
switch (e.getActionMasked()) {
|
|
122
127
|
case android.view.MotionEvent.ACTION_DOWN:
|
|
123
128
|
downX = e.getRawX(); downY = e.getRawY(); downT = e.getEventTime();
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { isAndroid, isIOS } from '@nativescript/core';
|
|
2
2
|
import { CustomWebView } from './custom-webview';
|
|
3
3
|
|
|
4
|
+
// params: any — the bridge payload is an untyped JSON object decoded from the WebView; each handler
|
|
5
|
+
// narrows it to its own param shape at the call site.
|
|
4
6
|
export type HandlerFn = (params: any) => unknown | Promise<unknown>;
|
|
5
7
|
|
|
6
8
|
interface RequestEnvelope {
|
|
@@ -76,10 +76,13 @@ export const MODULES: ModuleManifest[] = [
|
|
|
76
76
|
{ name: 'device', core: true, group: 'extended', capabilities: { device: 'native' } },
|
|
77
77
|
{ name: 'clipboard', core: true, group: 'extended', capabilities: { clipboard: 'native' } },
|
|
78
78
|
{ name: 'network', core: true, group: 'extended', capabilities: { network: 'native' } },
|
|
79
|
-
|
|
79
|
+
// privacyScreen: hide content in the app-switcher / block screenshots — permission-free, cheap → core.
|
|
80
|
+
{ name: 'screen', core: true, group: 'extended', capabilities: { screen: 'native', dialogs: 'native', orientation: 'native', keyboard: 'native', privacyScreen: { ios: true, android: true } } },
|
|
80
81
|
// badge: app-icon badge via the always-bundled notifications.setBadge handler — iOS sets the
|
|
81
82
|
// springboard badge; Android is an honest no-op (launchers own badges) → ios:true / android:false.
|
|
82
|
-
|
|
83
|
+
// app.canOpenUrl rides the always-native `app` cap (no new key). shortcuts: home-screen quick
|
|
84
|
+
// actions — iOS UIApplicationShortcutItem / Android dynamic shortcuts (API 25+).
|
|
85
|
+
{ name: 'app', core: true, group: 'system', capabilities: { app: 'native', browser: 'native', badge: { ios: true, android: false }, shortcuts: { ios: true, android: true } } },
|
|
83
86
|
|
|
84
87
|
// ── opt-in: permission / dependency / weight-bearing ───────────────────
|
|
85
88
|
// POST_NOTIFICATIONS / VIBRATE / USE_BIOMETRIC etc. are in the template's baseline manifest already.
|
|
@@ -246,11 +249,27 @@ export const MODULES: ModuleManifest[] = [
|
|
|
246
249
|
},
|
|
247
250
|
nativeSrc: 'health',
|
|
248
251
|
},
|
|
252
|
+
|
|
253
|
+
// ── backgroundTask — headless background execution (HEADLESS JS HANDLER) — opt-in, STRIPPABLE ──
|
|
254
|
+
// The OS wakes the app (possibly cold, no visible WebView) for a permitted task id; the shell builds
|
|
255
|
+
// an OFFSCREEN WebView, loads the app conveying the id (the handshake reports it), awaits the JS
|
|
256
|
+
// handler's `backgroundTask.finish`, then completes + reschedules the OS task. iOS: BGTaskScheduler
|
|
257
|
+
// (BGAppRefreshTaskRequest / BGProcessingTaskRequest) — NO gradle dep, but Info.plist MUST declare
|
|
258
|
+
// the permitted identifiers (`appwrap.json.backgroundTasks`, stamped by the CLI) + the fetch/processing
|
|
259
|
+
// background modes. Android: WorkManager periodic work (the work-runtime dep rides via this module),
|
|
260
|
+
// self-initialized by its androidx startup provider — nothing mandatory to stamp.
|
|
261
|
+
// DEVICE-UNVERIFIED: the native background-wake path (offscreen WebView under BGTask / WorkManager)
|
|
262
|
+
// compiles only; it has NOT been run on a device. See handlers-background.ts.
|
|
263
|
+
{
|
|
264
|
+
name: 'backgroundTask', group: 'backgroundTask',
|
|
265
|
+
capabilities: { backgroundTask: { ios: true, android: true } },
|
|
266
|
+
android: { gradleDeps: ['androidx.work:work-runtime:2.9.1'] },
|
|
267
|
+
},
|
|
249
268
|
];
|
|
250
269
|
|
|
251
270
|
/** Opt-in registration groups that own their own NS handler file (strippable when inactive). Core
|
|
252
271
|
* groups (core/extended/parity/system/media/billing) are always bundled; only these are CLI-gated. */
|
|
253
|
-
export const OPTIONAL_GROUPS = ['health', 'oauth', 'reviews', 'scanner', 'speech'] as const;
|
|
272
|
+
export const OPTIONAL_GROUPS = ['health', 'oauth', 'reviews', 'scanner', 'speech', 'backgroundTask'] as const;
|
|
254
273
|
|
|
255
274
|
/** Resolve the active capability map for the handshake from a set of active capability names. */
|
|
256
275
|
export function buildCapabilityMap(
|
|
@@ -5,7 +5,8 @@ import { APPWRAP_GLOBALS_JS, NATIVE_FEEL_JS, serviceWorkerGuardJs, externalNavGu
|
|
|
5
5
|
import { envGlobalsJs } from './env';
|
|
6
6
|
import { requestPermissions } from './android-helpers';
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
// `android` + `java` resolve to the real types-android namespaces (no declare needed).
|
|
9
|
+
declare const androidx: any; // no NS types: androidx.webkit.* (WebViewFeature/WebViewCompat) not in the android-32 platform typings
|
|
9
10
|
|
|
10
11
|
/** Stable in-app origin — secure context, ES modules work (file:// blocks them). */
|
|
11
12
|
export const APP_ORIGIN = 'https://appwrap.local';
|
|
@@ -42,7 +43,8 @@ export class CustomWebView extends WebView {
|
|
|
42
43
|
|
|
43
44
|
initNativeView(): void {
|
|
44
45
|
super.initNativeView();
|
|
45
|
-
|
|
46
|
+
// NS core types the `.android` getter as `any`; narrow to the real WebView for this scope.
|
|
47
|
+
const wv = this.android as android.webkit.WebView;
|
|
46
48
|
if (!wv) return;
|
|
47
49
|
|
|
48
50
|
// Debug mode only: debuggable via chrome://inspect (full console + network) + keep the screen
|
|
@@ -77,7 +79,7 @@ export class CustomWebView extends WebView {
|
|
|
77
79
|
* is process-wide but harmless with our single WebView. iOS suspends rAF on its own (no-op there).
|
|
78
80
|
*/
|
|
79
81
|
setRenderingActive(active: boolean): void {
|
|
80
|
-
const wv = this.android;
|
|
82
|
+
const wv = this.android as android.webkit.WebView;
|
|
81
83
|
if (!wv) return;
|
|
82
84
|
try {
|
|
83
85
|
if (active) { wv.onResume(); wv.resumeTimers(); }
|
|
@@ -87,11 +89,11 @@ export class CustomWebView extends WebView {
|
|
|
87
89
|
}
|
|
88
90
|
}
|
|
89
91
|
|
|
90
|
-
private installDocumentStartShim(wv:
|
|
92
|
+
private installDocumentStartShim(wv: android.webkit.WebView): void {
|
|
91
93
|
try {
|
|
92
94
|
const wkt = androidx.webkit;
|
|
93
95
|
if (wkt.WebViewFeature.isFeatureSupported(wkt.WebViewFeature.DOCUMENT_START_SCRIPT)) {
|
|
94
|
-
const rules = new java.util.HashSet();
|
|
96
|
+
const rules = new java.util.HashSet<string>();
|
|
95
97
|
rules.add(APP_ORIGIN);
|
|
96
98
|
wkt.WebViewCompat.addDocumentStartJavaScript(wv, buildBootstrapJs(), rules);
|
|
97
99
|
return;
|
|
@@ -102,13 +104,20 @@ export class CustomWebView extends WebView {
|
|
|
102
104
|
}
|
|
103
105
|
}
|
|
104
106
|
|
|
105
|
-
private createTransportChromeClient():
|
|
107
|
+
private createTransportChromeClient(): android.webkit.WebChromeClient {
|
|
106
108
|
const owner = new WeakRef<CustomWebView>(this);
|
|
109
|
+
// NS runtime adds `.extend` on Java classes; it's not in the static typings, hence the cast.
|
|
107
110
|
const ChromeClient = (android.webkit.WebChromeClient as any).extend({
|
|
108
|
-
onJsPrompt(
|
|
111
|
+
onJsPrompt(
|
|
112
|
+
_view: android.webkit.WebView,
|
|
113
|
+
_url: string,
|
|
114
|
+
message: string,
|
|
115
|
+
_defaultValue: string,
|
|
116
|
+
result: android.webkit.JsPromptResult
|
|
117
|
+
): boolean {
|
|
109
118
|
if (typeof message === 'string' && message.startsWith(PROMPT_PREFIX)) {
|
|
110
119
|
result.confirm('');
|
|
111
|
-
const view = owner.get ? owner.get() : (owner as
|
|
120
|
+
const view = owner.get ? owner.get() : (owner as { deref(): CustomWebView }).deref();
|
|
112
121
|
view?.onAppwrapMessage?.(message.slice(PROMPT_PREFIX.length));
|
|
113
122
|
return true;
|
|
114
123
|
}
|
|
@@ -117,7 +126,7 @@ export class CustomWebView extends WebView {
|
|
|
117
126
|
|
|
118
127
|
// getUserMedia: grant the WebView's per-origin capture after ensuring the
|
|
119
128
|
// app holds the matching OS runtime permission (CAMERA / RECORD_AUDIO).
|
|
120
|
-
onPermissionRequest(request:
|
|
129
|
+
onPermissionRequest(request: android.webkit.PermissionRequest): void {
|
|
121
130
|
const PR = android.webkit.PermissionRequest;
|
|
122
131
|
const resources: string[] = Array.from(request.getResources());
|
|
123
132
|
const perms = new Set<string>();
|
|
@@ -139,14 +148,15 @@ export class CustomWebView extends WebView {
|
|
|
139
148
|
}
|
|
140
149
|
|
|
141
150
|
/** https://appwrap.local/<path> → file under www/; SPA fallback to the entry for extension-less misses. */
|
|
142
|
-
function createAssetServingClient():
|
|
151
|
+
function createAssetServingClient(): android.webkit.WebViewClient {
|
|
143
152
|
const wwwPath = nsPath.join(knownFolders.currentApp().path, 'www');
|
|
144
153
|
|
|
145
|
-
const respond = (filePath: string, ext: string) => {
|
|
154
|
+
const respond = (filePath: string, ext: string): android.webkit.WebResourceResponse => {
|
|
146
155
|
const stream = new java.io.FileInputStream(filePath);
|
|
147
156
|
return new android.webkit.WebResourceResponse(mimeFor(ext, filePath), 'utf-8', stream);
|
|
148
157
|
};
|
|
149
158
|
|
|
159
|
+
// (Array as any).create is the NS interop helper for native byte[]; not in lib typings.
|
|
150
160
|
const emptyStream = () => new java.io.ByteArrayInputStream((Array as any).create('byte', 0));
|
|
151
161
|
|
|
152
162
|
/**
|
|
@@ -164,10 +174,16 @@ function createAssetServingClient(): any {
|
|
|
164
174
|
* (GET/HEAD) are proxied — POST/PUT/PATCH return 501 rather than silently sending an empty body.
|
|
165
175
|
* (BodDB writes ride the WS, not HTTP POST, so this is rarely hit.)
|
|
166
176
|
*/
|
|
167
|
-
const proxyToBackend = (
|
|
177
|
+
const proxyToBackend = (
|
|
178
|
+
rel: string,
|
|
179
|
+
query: string,
|
|
180
|
+
ext: string,
|
|
181
|
+
method: string,
|
|
182
|
+
headers: java.util.Map<string, string> | null
|
|
183
|
+
): android.webkit.WebResourceResponse => {
|
|
168
184
|
if (method !== 'GET' && method !== 'HEAD') {
|
|
169
185
|
return new android.webkit.WebResourceResponse(
|
|
170
|
-
'text/plain', 'utf-8', 501, 'Not Implemented', new java.util.HashMap(),
|
|
186
|
+
'text/plain', 'utf-8', 501, 'Not Implemented', new java.util.HashMap<string, string>(),
|
|
171
187
|
new java.io.ByteArrayInputStream(new java.lang.String(
|
|
172
188
|
'appwrap Android shell cannot proxy request bodies (shouldInterceptRequest limitation)'
|
|
173
189
|
).getBytes('UTF-8'))
|
|
@@ -178,9 +194,10 @@ function createAssetServingClient(): any {
|
|
|
178
194
|
const target = `${SHELL_CONFIG.backendOrigin.replace(/\/+$/, '')}/${rel}${query}`;
|
|
179
195
|
const pump = new java.lang.Runnable({
|
|
180
196
|
run() {
|
|
181
|
-
let is:
|
|
197
|
+
let is: java.io.InputStream | null = null;
|
|
182
198
|
try {
|
|
183
|
-
|
|
199
|
+
// openConnection() statically returns URLConnection; for http(s) it's an HttpURLConnection.
|
|
200
|
+
const conn = new java.net.URL(target).openConnection() as java.net.HttpURLConnection;
|
|
184
201
|
conn.setRequestMethod(method);
|
|
185
202
|
conn.setInstanceFollowRedirects(true);
|
|
186
203
|
conn.setConnectTimeout(15000);
|
|
@@ -209,17 +226,22 @@ function createAssetServingClient(): any {
|
|
|
209
226
|
},
|
|
210
227
|
});
|
|
211
228
|
new java.lang.Thread(pump).start();
|
|
212
|
-
const outHeaders = new java.util.HashMap();
|
|
229
|
+
const outHeaders = new java.util.HashMap<string, string>();
|
|
213
230
|
outHeaders.put('Access-Control-Allow-Origin', '*');
|
|
214
231
|
outHeaders.put('Cache-Control', 'no-cache');
|
|
215
232
|
// mime/charset best-effort from the path; status 200 (real status is unknown until the bg read).
|
|
216
233
|
return new android.webkit.WebResourceResponse(mimeFor(ext, rel), 'utf-8', 200, 'OK', outHeaders, pin);
|
|
217
234
|
};
|
|
218
235
|
|
|
236
|
+
// NS runtime adds `.extend` on Java classes; it's not in the static typings, hence the cast.
|
|
219
237
|
const Client = (android.webkit.WebViewClient as any).extend({
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
238
|
+
// Modern (API 21+) overload passes a WebResourceRequest; the legacy overload passed a string URL.
|
|
239
|
+
// We defensively handle both, so the param is the union.
|
|
240
|
+
shouldInterceptRequest(
|
|
241
|
+
_view: android.webkit.WebView,
|
|
242
|
+
request: android.webkit.WebResourceRequest | string
|
|
243
|
+
): android.webkit.WebResourceResponse | null {
|
|
244
|
+
const url: string = typeof request !== 'string' ? String(request.getUrl().toString()) : request;
|
|
223
245
|
if (!url.startsWith(APP_ORIGIN)) return null; // external — let WebView handle it
|
|
224
246
|
|
|
225
247
|
const tail = url.slice(APP_ORIGIN.length);
|
|
@@ -239,25 +261,30 @@ function createAssetServingClient(): any {
|
|
|
239
261
|
// Not a local asset → proxy to the backend (mirror of iOS), making relative
|
|
240
262
|
// backend/vendor requests same-origin under app://appwrap.local.
|
|
241
263
|
if (SHELL_CONFIG.backendOrigin) {
|
|
264
|
+
const isObj = typeof request !== 'string';
|
|
242
265
|
const method = (isObj && request.getMethod && String(request.getMethod())) || 'GET';
|
|
243
266
|
const headers = isObj && request.getRequestHeaders ? request.getRequestHeaders() : null;
|
|
244
267
|
return proxyToBackend(rel, query, ext, method.toUpperCase(), headers);
|
|
245
268
|
}
|
|
246
269
|
return new android.webkit.WebResourceResponse(
|
|
247
|
-
'text/plain', 'utf-8', 404, 'Not Found', new java.util.HashMap(), emptyStream()
|
|
270
|
+
'text/plain', 'utf-8', 404, 'Not Found', new java.util.HashMap<string, string>(), emptyStream()
|
|
248
271
|
);
|
|
249
272
|
},
|
|
250
273
|
|
|
251
|
-
onPageStarted(view:
|
|
274
|
+
onPageStarted(view: android.webkit.WebView, _url: string, _favicon: android.graphics.Bitmap): void {
|
|
252
275
|
// Fallback injection — no-op when the document-start scripts already ran
|
|
253
|
-
view.evaluateJavascript(buildBootstrapJs(), null);
|
|
276
|
+
view.evaluateJavascript(buildBootstrapJs(), null as unknown as android.webkit.ValueCallback<string>);
|
|
254
277
|
},
|
|
255
278
|
|
|
256
279
|
// `appwrap dev` points at a LAN dev server that almost always uses a self-signed / mkcert TLS
|
|
257
280
|
// cert the device's trust store doesn't know — the WebView would otherwise hard-fail with
|
|
258
281
|
// ERR_CERT_AUTHORITY_INVALID. Proceed past it ONLY in a debug build AND only when actually in
|
|
259
282
|
// server-loader (dev) mode. Production app:// builds never reach this (local assets, no TLS).
|
|
260
|
-
onReceivedSslError(
|
|
283
|
+
onReceivedSslError(
|
|
284
|
+
_view: android.webkit.WebView,
|
|
285
|
+
handler: android.webkit.SslErrorHandler,
|
|
286
|
+
error: android.net.http.SslError
|
|
287
|
+
): void {
|
|
261
288
|
if (SHELL_CONFIG.debug && SHELL_CONFIG.loader === 'server') {
|
|
262
289
|
console.warn('AppWrap: proceeding past dev-server SSL error (debug+server only):', String(error));
|
|
263
290
|
handler.proceed();
|