@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.
Files changed (40) hide show
  1. package/package.json +1 -1
  2. package/runtime/App_Resources/Android/src/main/AndroidManifest.xml +3 -0
  3. package/runtime/app/app.ts +57 -39
  4. package/runtime/app/shell/android-helpers.ts +6 -6
  5. package/runtime/app/shell/background-bootstrap.generated.ts +8 -0
  6. package/runtime/app/shell/background-context.ts +20 -0
  7. package/runtime/app/shell/banner.ts +9 -4
  8. package/runtime/app/shell/bridge.ts +2 -0
  9. package/runtime/app/shell/capabilities.manifest.ts +22 -3
  10. package/runtime/app/shell/custom-webview.android.ts +50 -23
  11. package/runtime/app/shell/custom-webview.ios.ts +90 -78
  12. package/runtime/app/shell/devmenu.ts +2 -2
  13. package/runtime/app/shell/env.ts +1 -3
  14. package/runtime/app/shell/events.ts +25 -4
  15. package/runtime/app/shell/fcm-firebase.d.ts +2 -1
  16. package/runtime/app/shell/handlers-android.ts +16 -12
  17. package/runtime/app/shell/handlers-background.ts +290 -0
  18. package/runtime/app/shell/handlers-billing.ts +90 -82
  19. package/runtime/app/shell/handlers-extended.ts +141 -110
  20. package/runtime/app/shell/handlers-fs.ts +53 -41
  21. package/runtime/app/shell/handlers-health.ts +27 -19
  22. package/runtime/app/shell/handlers-media.ts +8 -14
  23. package/runtime/app/shell/handlers-oauth.ts +19 -19
  24. package/runtime/app/shell/handlers-parity.ts +118 -89
  25. package/runtime/app/shell/handlers-push.ts +5 -15
  26. package/runtime/app/shell/handlers-reviews.ts +5 -5
  27. package/runtime/app/shell/handlers-scanner.ts +36 -38
  28. package/runtime/app/shell/handlers-speech.ts +43 -49
  29. package/runtime/app/shell/handlers-system.ts +122 -1
  30. package/runtime/app/shell/handlers.ts +10 -3
  31. package/runtime/app/shell/ios-frameworks.d.ts +9 -44
  32. package/runtime/app/shell/ios-ui-delegate.ts +112 -108
  33. package/runtime/app/shell/native-log.ts +1 -3
  34. package/runtime/app/shell/status-bar.ts +7 -4
  35. package/runtime/app/shell/toast.ts +1 -1
  36. package/runtime/references.d.ts +21 -0
  37. package/runtime/tsconfig.json +1 -0
  38. package/src/cli.ts +74 -18
  39. package/src/config.ts +20 -0
  40. package/src/derive.ts +75 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livx.cc/appwrap",
3
- "version": "0.26.0",
3
+ "version": "0.28.1",
4
4
  "description": "Wrap any PWA into a native app with native capabilities (appwrap runtime + @livx.cc/native-kit).",
5
5
  "license": "MIT",
6
6
  "author": "Elya Livshitz",
@@ -19,6 +19,9 @@
19
19
  <!-- appwrap:permissions -->
20
20
  <!-- /appwrap:permissions -->
21
21
 
22
+ <!-- appwrap:queries -->
23
+ <!-- /appwrap:queries -->
24
+
22
25
  <application
23
26
  android:name="com.tns.NativeScriptApplication"
24
27
  android:allowBackup="true"
@@ -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
- const DelegateClass = (UIResponder as any).extend(
19
- {
20
- applicationDidFinishLaunchingWithOptions(_app: UIApplication, launchOptions: NSDictionary<string, any>) {
21
- // Set the notification delegate NOW so a cold-launch tap's deep link is delivered.
22
- installForegroundNotificationDelegate();
23
- const url = launchOptions?.objectForKey(UIApplicationLaunchOptionsURLKey);
24
- if (url) onDeepLink(url.absoluteString ?? String(url));
25
- return true;
26
- },
27
- applicationOpenURLOptions(_app: UIApplication, url: NSURL, _options: NSDictionary<string, any>) {
28
- console.log('AppWrap: openURL', url.absoluteString);
29
- onDeepLink(url.absoluteString);
30
- return true;
31
- },
32
- // Drives `kit.screen.orientation.lock` — UIKit reads this on every rotation.
33
- applicationSupportedInterfaceOrientationsForWindow(_app: UIApplication, _window: any) {
34
- return iosOrientationMask();
35
- },
36
- // ── Remote push (APNs) — delivered to the AppDelegate, bridged to kit.push. ──
37
- applicationDidRegisterForRemoteNotificationsWithDeviceToken(_app: UIApplication, deviceToken: NSData) {
38
- onApnsToken(apnsTokenToHex(deviceToken));
39
- },
40
- applicationDidFailToRegisterForRemoteNotificationsWithError(_app: UIApplication, error: NSError) {
41
- onApnsError(error?.localizedDescription ?? 'APNs registration failed');
42
- },
43
- // Background/silent + content-available pushes (a tapped alert routes via the UN delegate).
44
- applicationDidReceiveRemoteNotificationFetchCompletionHandler(
45
- _app: UIApplication,
46
- userInfo: NSDictionary<any, any>,
47
- completionHandler: (result: UIBackgroundFetchResult) => void
48
- ) {
49
- onRemoteMessage(userInfo, false);
50
- completionHandler(UIBackgroundFetchResult.NewData);
51
- },
52
- },
53
- { protocols: [UIApplicationDelegate] }
54
- );
55
- Application.ios.delegate = DelegateClass;
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(): any {
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: any, maxSize: number): string | null {
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: any): string {
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: any): Promise<{ resultCode: number; intent: any }> {
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
- // Built lazily (and only on iOS) — `NSObject`/`interop` don't exist on Android, and this file is
45
- // imported on both platforms via handlers.ts, so a top-level `.extend` would crash the Android shell.
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: any, e: any) {
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
- { name: 'screen', core: true, group: 'extended', capabilities: { screen: 'native', dialogs: 'native', orientation: 'native', keyboard: 'native' } },
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
- { name: 'app', core: true, group: 'system', capabilities: { app: 'native', browser: 'native', badge: { ios: true, android: false } } },
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
- declare const android: any, java: any, androidx: any;
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
- const wv = this.android;
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: any): void {
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(): any {
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(_view: any, _url: string, message: string, _defaultValue: string, result: any): boolean {
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 any).deref();
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: any): void {
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(): any {
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 = (rel: string, query: string, ext: string, method: string, headers: any): any => {
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: any = null;
197
+ let is: java.io.InputStream | null = null;
182
198
  try {
183
- const conn = new java.net.URL(target).openConnection();
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
- shouldInterceptRequest(_view: any, request: any): any {
221
- const isObj = typeof request !== 'string';
222
- const url: string = isObj ? String(request.getUrl().toString()) : request;
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: any, _url: string, _favicon: any): void {
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(_view: any, handler: any, error: any): void {
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();