@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
@@ -18,23 +18,55 @@ import { appwrapNativeLog } from './native-log';
18
18
  const appendWebLog = appwrapNativeLog;
19
19
 
20
20
  /** Parse a #rrggbb config color into a UIColor (for the WebView background). */
21
- function uiColorFromHex(hex?: string): any {
21
+ function uiColorFromHex(hex?: string): UIColor | null {
22
22
  const m = /^#?([0-9a-fA-F]{6})$/.exec((hex ?? '').trim());
23
23
  if (!m) return null;
24
24
  const n = parseInt(m[1], 16);
25
25
  return UIColor.colorWithRedGreenBlueAlpha(((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255, 1);
26
26
  }
27
27
 
28
+ /** WKScriptMessageHandler for the `appwrap` channel — forwards JSON envelopes to the owning view. */
29
+ @NativeClass()
30
+ class AppwrapScriptHandler extends NSObject implements WKScriptMessageHandler {
31
+ static ObjCProtocols = [WKScriptMessageHandler];
32
+ owner!: WeakRef<CustomWebView>;
33
+ static new(): AppwrapScriptHandler {
34
+ return <AppwrapScriptHandler>super.new();
35
+ }
36
+ userContentControllerDidReceiveScriptMessage(_controller: WKUserContentController, message: WKScriptMessage): void {
37
+ const view = this.owner.get ? this.owner.get() : (this.owner as { deref(): CustomWebView }).deref();
38
+ const body = message.body;
39
+ // Envelopes always travel as JSON strings (protocol v1)
40
+ if (view?.onAppwrapMessage && typeof body === 'string') {
41
+ view.onAppwrapMessage(body);
42
+ }
43
+ }
44
+ }
45
+
46
+ /** WKScriptMessageHandler for the `appwrapLog` channel — debug console-forwarding to the file sink. */
47
+ @NativeClass()
48
+ class AppwrapLogHandler extends NSObject implements WKScriptMessageHandler {
49
+ static ObjCProtocols = [WKScriptMessageHandler];
50
+ static new(): AppwrapLogHandler {
51
+ return <AppwrapLogHandler>super.new();
52
+ }
53
+ userContentControllerDidReceiveScriptMessage(_c: WKUserContentController, message: WKScriptMessage): void {
54
+ // Write to the file sink — NS console.log / NSLog don't surface to devicectl/idevicesyslog
55
+ // on a device build, but a file in the app container is reliably pullable.
56
+ if (typeof message.body === 'string') appendWebLog(message.body);
57
+ }
58
+ }
59
+
28
60
  export class CustomWebView extends WebView {
29
61
  /** Set by the bridge before load; receives raw envelope JSON. */
30
62
  onAppwrapMessage: ((json: string) => void) | null = null;
31
- private _scriptHandler: any; // retained — WKUserContentController holds it weakly
32
- private _logHandler: any; // retained — debug console-forwarding handler
33
- private _schemeHandler: any;
63
+ private _scriptHandler!: AppwrapScriptHandler; // retained — WKUserContentController holds it weakly
64
+ private _logHandler!: AppwrapLogHandler; // retained — debug console-forwarding handler
65
+ private _schemeHandler!: WKURLSchemeHandler;
34
66
  // NOT `_uiDelegate`: the base NativeScript WebView uses that exact field name, and
35
67
  // TS `private` is compile-time only — sharing it let super.initNativeView() overwrite
36
68
  // our media-capture delegate with NS's, so getUserMedia re-prompted every call.
37
- private _appwrapUiDelegate: any; // retained — WKWebView holds uiDelegate weakly
69
+ private _appwrapUiDelegate!: WKUIDelegate; // retained — WKWebView holds uiDelegate weakly
38
70
 
39
71
  /** No-op on iOS: WKWebView suspends rAF/timers itself when the app leaves the foreground.
40
72
  * Kept for parity with the Android impl (wired to suspend/resume in main-page). */
@@ -67,39 +99,14 @@ export class CustomWebView extends WebView {
67
99
  }
68
100
 
69
101
  const owner = new WeakRef<CustomWebView>(this);
70
- const HandlerClass = (NSObject as any).extend(
71
- {
72
- userContentControllerDidReceiveScriptMessage(
73
- _controller: WKUserContentController,
74
- message: WKScriptMessage
75
- ): void {
76
- const view = owner.get ? owner.get() : (owner as any).deref();
77
- const body = message.body;
78
- // Envelopes always travel as JSON strings (protocol v1)
79
- if (view?.onAppwrapMessage && typeof body === 'string') {
80
- view.onAppwrapMessage(body);
81
- }
82
- },
83
- },
84
- { protocols: [WKScriptMessageHandler] }
85
- );
86
- this._scriptHandler = HandlerClass.new();
102
+ this._scriptHandler = AppwrapScriptHandler.new();
103
+ this._scriptHandler.owner = owner;
87
104
  config.userContentController.addScriptMessageHandlerName(this._scriptHandler, 'appwrap');
88
105
 
89
106
  // Debug console-forwarding: web console/errors → NSLog (via NS console.log) → device console,
90
107
  // so `appwrap logs ios` can stream them headlessly. Only wired in debug mode.
91
108
  if (SHELL_CONFIG.debug) {
92
- const LogClass = (NSObject as any).extend(
93
- {
94
- userContentControllerDidReceiveScriptMessage(_c: WKUserContentController, message: WKScriptMessage): void {
95
- // Write to the file sink — NS console.log / NSLog don't surface to devicectl/idevicesyslog
96
- // on a device build, but a file in the app container is reliably pullable.
97
- if (typeof message.body === 'string') appendWebLog(message.body);
98
- },
99
- },
100
- { protocols: [WKScriptMessageHandler] }
101
- );
102
- this._logHandler = LogClass.new();
109
+ this._logHandler = AppwrapLogHandler.new();
103
110
  config.userContentController.addScriptMessageHandlerName(this._logHandler, 'appwrapLog');
104
111
  appendWebLog('[native] log handler registered'); // boot marker: proves the file sink works
105
112
  }
@@ -124,13 +131,13 @@ export class CustomWebView extends WebView {
124
131
  // Edge-swipe to go back/forward in WebView history — the iOS native gesture
125
132
  // users expect. SPAs that own their own history still get it for free.
126
133
  webView.allowsBackForwardNavigationGestures = true;
127
- if ((webView.configuration as any).dataDetectorTypes !== undefined) {
128
- (webView.configuration as any).dataDetectorTypes = 0; // WKDataDetectorTypeNone
134
+ if (webView.configuration.dataDetectorTypes !== undefined) {
135
+ webView.configuration.dataDetectorTypes = 0; // WKDataDetectorTypeNone (WKDataDetectorTypes.None)
129
136
  }
130
137
 
131
138
  // Observability (debug mode only): make the WebView debuggable via Safari Web Inspector
132
139
  // (Develop → <device> → Para) — full console + network. iOS 16.4+ (guarded). Off in store builds.
133
- if (SHELL_CONFIG.debug && webView.respondsToSelector('setInspectable:')) (webView as any).inspectable = true;
140
+ if (SHELL_CONFIG.debug && webView.respondsToSelector('setInspectable:')) webView.inspectable = true;
134
141
 
135
142
  this._appwrapUiDelegate = createUiDelegate();
136
143
  webView.UIDelegate = this._appwrapUiDelegate;
@@ -147,9 +154,9 @@ export class CustomWebView extends WebView {
147
154
  const wk = this.nativeViewProtected as WKWebView;
148
155
  if (wk && this._appwrapUiDelegate) wk.UIDelegate = this._appwrapUiDelegate;
149
156
  // Lightweight self-check surfaced via debug.webviewInfo (support diagnostics).
150
- (global as any).__appwrapWebviewDiag =
157
+ (global as { __appwrapWebviewDiag?: string }).__appwrapWebviewDiag =
151
158
  `delegateIsMine=${!!wk && wk.UIDelegate === this._appwrapUiDelegate} ` +
152
- `respondsToMedia=${!!wk && !!wk.UIDelegate && !!(wk.UIDelegate as any).respondsToSelector?.('webView:requestMediaCapturePermissionForOrigin:initiatedByFrame:type:decisionHandler:')}`;
159
+ `respondsToMedia=${!!wk && !!wk.UIDelegate && !!wk.UIDelegate.respondsToSelector?.('webView:requestMediaCapturePermissionForOrigin:initiatedByFrame:type:decisionHandler:')}`;
153
160
  }
154
161
  }
155
162
 
@@ -163,41 +170,42 @@ export class CustomWebView extends WebView {
163
170
  * request body, so POSTs proxy fine.
164
171
  * - else → 404 (or SPA fallback when there's no backend).
165
172
  */
166
- function createAppSchemeHandler(): any {
173
+ function createAppSchemeHandler(): WKURLSchemeHandler {
167
174
  const wwwPath = nsPath.join(knownFolders.currentApp().path, 'www');
168
175
 
169
- const serveFile = (task: any, url: any, filePath: string, ext: string): void => {
176
+ const serveFile = (task: WKURLSchemeTask, url: NSURL, filePath: string, ext: string): void => {
170
177
  const data = NSData.dataWithContentsOfFile(filePath);
171
178
  const mime = mimeFor(ext, filePath);
172
179
  const response = NSHTTPURLResponse.alloc().initWithURLStatusCodeHTTPVersionHeaderFields(
173
180
  url, 200, 'HTTP/1.1',
181
+ // NS auto-marshals this JS object into NSDictionary<string,string>
174
182
  {
175
183
  'Content-Type': `${mime}; charset=utf-8`,
176
184
  'Content-Length': String(data?.length ?? 0),
177
185
  'Access-Control-Allow-Origin': '*',
178
186
  'Cache-Control': 'no-cache',
179
- } as any
187
+ } as unknown as NSDictionary<string, string>
180
188
  );
181
189
  task.didReceiveResponse(response);
182
190
  if (data) task.didReceiveData(data);
183
191
  task.didFinish();
184
192
  };
185
193
 
186
- const proxyToBackend = (task: any, url: any, rel: string): void => {
194
+ const proxyToBackend = (task: WKURLSchemeTask, url: NSURL, rel: string): void => {
187
195
  const backend = SHELL_CONFIG.backendOrigin.replace(/\/+$/, '');
188
196
  const query = url.query ? `?${url.query}` : '';
189
197
  const req = NSMutableURLRequest.requestWithURL(NSURL.URLWithString(`${backend}/${rel}${query}`));
190
198
  req.HTTPMethod = String(task.request.HTTPMethod ?? 'GET');
191
199
  const hdrs = task.request.allHTTPHeaderFields;
192
200
  if (hdrs) req.allHTTPHeaderFields = hdrs;
193
- req.setValueForHTTPHeaderField(null as any, 'Host'); // let URLSession set Host from the target URL
201
+ req.setValueForHTTPHeaderField(null as unknown as string, 'Host'); // let URLSession set Host from the target URL
194
202
  // WKURLSchemeHandler delivers a POST/PUT body via HTTPBodyStream (not HTTPBody) — read it.
195
- let body = task.request.HTTPBody;
203
+ let body: NSData = task.request.HTTPBody;
196
204
  if (!body && task.request.HTTPBodyStream) {
197
205
  const stream = task.request.HTTPBodyStream;
198
206
  const acc = NSMutableData.alloc().init();
199
207
  const cap = 16384;
200
- const buf = (global as any).interop.alloc(cap);
208
+ const buf = interop.alloc(cap);
201
209
  stream.open();
202
210
  while (stream.hasBytesAvailable) {
203
211
  const n = stream.readMaxLength(buf, cap);
@@ -211,11 +219,11 @@ function createAppSchemeHandler(): any {
211
219
 
212
220
  const dataTask = NSURLSession.sharedSession.dataTaskWithRequestCompletionHandler(
213
221
  req,
214
- (data: any, resp: any, error: any) => {
222
+ (data: NSData, resp: NSURLResponse, error: NSError) => {
215
223
  try {
216
224
  if (error || !resp) {
217
225
  const r = NSHTTPURLResponse.alloc().initWithURLStatusCodeHTTPVersionHeaderFields(
218
- url, 502, 'HTTP/1.1', { 'Content-Length': '0' } as any
226
+ url, 502, 'HTTP/1.1', { 'Content-Length': '0' } as unknown as NSDictionary<string, string>
219
227
  );
220
228
  task.didReceiveResponse(r);
221
229
  task.didFinish();
@@ -223,10 +231,12 @@ function createAppSchemeHandler(): any {
223
231
  }
224
232
  // Re-emit under the app:// URL (same-origin). Strip Content-Encoding (URLSession already
225
233
  // decoded the body) and recompute length; keep the upstream Content-Type + status.
226
- const ct = resp.valueForHTTPHeaderField?.('Content-Type') ?? 'application/octet-stream';
234
+ // resp is NSHTTPURLResponse at runtime (HTTP request) — cast for the HTTP-only members.
235
+ const httpResp = resp as NSHTTPURLResponse;
236
+ const ct = httpResp.valueForHTTPHeaderField?.('Content-Type') ?? 'application/octet-stream';
227
237
  const out = NSHTTPURLResponse.alloc().initWithURLStatusCodeHTTPVersionHeaderFields(
228
- url, resp.statusCode ?? 200, 'HTTP/1.1',
229
- { 'Content-Type': ct, 'Content-Length': String(data?.length ?? 0), 'Cache-Control': 'no-cache' } as any
238
+ url, httpResp.statusCode ?? 200, 'HTTP/1.1',
239
+ { 'Content-Type': ct, 'Content-Length': String(data?.length ?? 0), 'Cache-Control': 'no-cache' } as unknown as NSDictionary<string, string>
230
240
  );
231
241
  task.didReceiveResponse(out);
232
242
  if (data) task.didReceiveData(data);
@@ -239,36 +249,38 @@ function createAppSchemeHandler(): any {
239
249
  dataTask.resume();
240
250
  };
241
251
 
242
- const HandlerClass = (NSObject as any).extend(
243
- {
244
- webViewStartURLSchemeTask(_webView: WKWebView, task: any): void {
245
- const url = task.request.URL;
246
- let rel = decodeURIComponent(String(url.path ?? '/')).replace(/^\/+/, '');
247
- if (!rel) rel = SHELL_CONFIG.entry;
248
- const filePath = nsPath.join(wwwPath, rel);
249
- const ext = rel.includes('.') ? rel.split('.').pop()!.toLowerCase() : '';
252
+ @NativeClass()
253
+ class AppSchemeHandler extends NSObject implements WKURLSchemeHandler {
254
+ static ObjCProtocols = [WKURLSchemeHandler];
255
+ static new(): AppSchemeHandler {
256
+ return <AppSchemeHandler>super.new();
257
+ }
258
+ webViewStartURLSchemeTask(_webView: WKWebView, task: WKURLSchemeTask): void {
259
+ const url = task.request.URL;
260
+ let rel = decodeURIComponent(String(url.path ?? '/')).replace(/^\/+/, '');
261
+ if (!rel) rel = SHELL_CONFIG.entry;
262
+ const filePath = nsPath.join(wwwPath, rel);
263
+ const ext = rel.includes('.') ? rel.split('.').pop()!.toLowerCase() : '';
250
264
 
251
- if (File.exists(filePath)) { serveFile(task, url, filePath, ext); return; }
265
+ if (File.exists(filePath)) { serveFile(task, url, filePath, ext); return; }
252
266
 
253
- const method = String(task.request.HTTPMethod ?? 'GET').toUpperCase();
254
- const accept = task.request.valueForHTTPHeaderField?.('Accept') ?? '';
255
- const isNavigation = method === 'GET' && accept.indexOf('text/html') >= 0;
267
+ const method = String(task.request.HTTPMethod ?? 'GET').toUpperCase();
268
+ const accept = task.request.valueForHTTPHeaderField?.('Accept') ?? '';
269
+ const isNavigation = method === 'GET' && accept.indexOf('text/html') >= 0;
256
270
 
257
- if (isNavigation && !ext) { serveFile(task, url, nsPath.join(wwwPath, SHELL_CONFIG.entry), 'html'); return; }
258
- if (SHELL_CONFIG.backendOrigin) { proxyToBackend(task, url, rel); return; }
259
- if (!ext) { serveFile(task, url, nsPath.join(wwwPath, SHELL_CONFIG.entry), 'html'); return; }
271
+ if (isNavigation && !ext) { serveFile(task, url, nsPath.join(wwwPath, SHELL_CONFIG.entry), 'html'); return; }
272
+ if (SHELL_CONFIG.backendOrigin) { proxyToBackend(task, url, rel); return; }
273
+ if (!ext) { serveFile(task, url, nsPath.join(wwwPath, SHELL_CONFIG.entry), 'html'); return; }
260
274
 
261
- const resp = NSHTTPURLResponse.alloc().initWithURLStatusCodeHTTPVersionHeaderFields(
262
- url, 404, 'HTTP/1.1', { 'Content-Length': '0' } as any
263
- );
264
- task.didReceiveResponse(resp);
265
- task.didFinish();
266
- },
267
- webViewStopURLSchemeTask(_webView: WKWebView, _task: any): void {
268
- // single-shot responses; in-flight proxy completions are guarded by try/catch.
269
- },
270
- },
271
- { protocols: [WKURLSchemeHandler] }
272
- );
273
- return HandlerClass.new();
275
+ const resp = NSHTTPURLResponse.alloc().initWithURLStatusCodeHTTPVersionHeaderFields(
276
+ url, 404, 'HTTP/1.1', { 'Content-Length': '0' } as unknown as NSDictionary<string, string>
277
+ );
278
+ task.didReceiveResponse(resp);
279
+ task.didFinish();
280
+ }
281
+ webViewStopURLSchemeTask(_webView: WKWebView, _task: WKURLSchemeTask): void {
282
+ // single-shot responses; in-flight proxy completions are guarded by try/catch.
283
+ }
284
+ }
285
+ return AppSchemeHandler.new();
274
286
  }
@@ -19,7 +19,7 @@ export function startDevMenu(): void {
19
19
 
20
20
  // ── shake detection (total acceleration magnitude in g incl. gravity, debounced) ──
21
21
  const SHAKE_G = 1.8; // spike vs. ~1g rest
22
- let shakeMm: any = null; // hold a ref so CoreMotion isn't GC'd
22
+ let shakeMm: CMMotionManager | null = null; // hold a ref so CoreMotion isn't GC'd
23
23
  let firstSpikeAt = 0;
24
24
  let spikes = 0;
25
25
  let lastMenuAt = 0;
@@ -68,7 +68,7 @@ function startAndroidShake(): void {
68
68
  const G = 9.80665;
69
69
  const listener = new android.hardware.SensorEventListener({
70
70
  onAccuracyChanged() {},
71
- onSensorChanged(e: any) {
71
+ onSensorChanged(e: android.hardware.SensorEvent) {
72
72
  const v = e.values;
73
73
  onAccelMagnitude(Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]) / G);
74
74
  },
@@ -1,7 +1,5 @@
1
1
  import { Application, isAndroid } from '@nativescript/core';
2
2
 
3
- declare const android: any, NSProcessInfo: any, UIAccessibilityIsReduceMotionEnabled: any;
4
-
5
3
  /**
6
4
  * Runtime environment hints the page can't reliably detect itself, surfaced natively so a PWA can
7
5
  * scale its rendering down inside the shell. Injected at document-start as `window.__APPWRAP__` (see
@@ -36,7 +34,7 @@ export function envGlobalsJs(): string {
36
34
  function androidIsEmulator(): boolean {
37
35
  try {
38
36
  const B = android.os.Build;
39
- const has = (v: any, s: string) => String(v || '').toLowerCase().includes(s);
37
+ const has = (v: string, s: string) => String(v || '').toLowerCase().includes(s);
40
38
  return (
41
39
  has(B.FINGERPRINT, 'generic') || has(B.FINGERPRINT, 'emulator') || has(B.FINGERPRINT, 'unknown') ||
42
40
  has(B.MODEL, 'google_sdk') || has(B.MODEL, 'emulator') || has(B.MODEL, 'android sdk built for') ||
@@ -1,9 +1,11 @@
1
1
  import { Application, Connectivity, isAndroid } from '@nativescript/core';
2
+ import type { AndroidActivityNewIntentEventData, OrientationChangedEventData } from '@nativescript/core';
2
3
  import { bridge } from './bridge';
3
4
  import { connectivityStatus } from './handlers-extended';
4
5
 
5
6
  let pendingDeepLink: string | null = null;
6
7
  let pendingPushTap: { data: Record<string, string> } | null = null;
8
+ let pendingShortcut: string | null = null;
7
9
  // True only once the PWA's JS has handshaked — i.e. the WebView is actually
8
10
  // running our bundle and about to subscribe. Native page-load is too early:
9
11
  // a deeplink.open emitted then lands in a WebView with no listener and is lost
@@ -27,6 +29,15 @@ export function onDeepLink(url: string): void {
27
29
  else pendingDeepLink = url; // buffer until the PWA handshakes
28
30
  }
29
31
 
32
+ /** A home-screen shortcut was activated (iOS performActionForShortcutItem / cold-start launchOptions;
33
+ * Android the launch intent's `appwrap_shortcut` extra). Buffered until the handshake like deep links
34
+ * (cold-start: a shortcut that launched the app would otherwise land in a WebView with no listener). */
35
+ export function onShortcut(id: string): void {
36
+ if (!id) return;
37
+ if (pwaReady) bridge.emit('app.shortcut', { id });
38
+ else pendingShortcut = id;
39
+ }
40
+
30
41
  /** Android: a tray notification (FCM) was tapped → re-launched the activity with the data payload as
31
42
  * intent extras. Buffered until handshake like deep links (cold-start-from-notification). iOS routes
32
43
  * taps via the AppDelegate (handlers-push onRemoteMessage). */
@@ -53,13 +64,18 @@ export function onPwaHandshake(): void {
53
64
  pendingPushTap = null;
54
65
  setTimeout(() => bridge.emit('push.tap', payload), 500);
55
66
  }
67
+ if (pendingShortcut) {
68
+ const id = pendingShortcut;
69
+ pendingShortcut = null;
70
+ setTimeout(() => bridge.emit('app.shortcut', { id }), 500);
71
+ }
56
72
  }
57
73
 
58
74
  /** Wire lifecycle + connectivity event forwarding (the PWA subscribes to these). */
59
75
  export function startEventForwarding(): void {
60
76
  Application.on(Application.suspendEvent, () => bridge.emit('app.pause'));
61
77
  Application.on(Application.resumeEvent, () => bridge.emit('app.resume'));
62
- Application.on(Application.orientationChangedEvent, (args: any) =>
78
+ Application.on(Application.orientationChangedEvent, (args: OrientationChangedEventData) =>
63
79
  bridge.emit('screen.orientation.change', args?.newValue === 'landscape' ? 'landscape' : 'portrait')
64
80
  );
65
81
 
@@ -71,18 +87,23 @@ export function startEventForwarding(): void {
71
87
  /** Intents: launch intent (cold start) + onNewIntent (warm, singleTask). Carries both VIEW deep links
72
88
  * and FCM notification-tap extras. */
73
89
  function wireAndroidDeepLinks(): void {
74
- const emitFromIntent = (intent: any) => {
90
+ const emitFromIntent = (intent: android.content.Intent | null | undefined) => {
75
91
  try {
76
92
  const data = intent?.getData?.();
77
93
  if (data) onDeepLink(String(data.toString()));
78
94
  const tap = readFcmTapExtras(intent);
79
95
  if (tap) onPushTap(tap);
96
+ const shortcutId = intent?.getStringExtra?.('appwrap_shortcut');
97
+ if (shortcutId) {
98
+ onShortcut(String(shortcutId));
99
+ intent.removeExtra?.('appwrap_shortcut'); // consume — don't re-fire on the next relayout read
100
+ }
80
101
  } catch (e) {
81
102
  console.warn('AppWrap: intent read failed', e);
82
103
  }
83
104
  };
84
105
 
85
- Application.android.on(Application.android.activityNewIntentEvent as any, (args: any) =>
106
+ Application.android.on(Application.android.activityNewIntentEvent, (args: AndroidActivityNewIntentEventData) =>
86
107
  emitFromIntent(args.intent)
87
108
  );
88
109
  emitFromIntent(Application.android.startActivity?.getIntent?.());
@@ -91,7 +112,7 @@ function wireAndroidDeepLinks(): void {
91
112
  /** A tapped FCM notification re-launches the activity with the message's data payload as string
92
113
  * extras, alongside FCM's own `google.*`/`gcm.*`/`from`/`collapse_key` bookkeeping keys. Detect via
93
114
  * those markers; return the app's data keys only. Null when this isn't an FCM-originated intent. */
94
- function readFcmTapExtras(intent: any): { data: Record<string, string> } | null {
115
+ function readFcmTapExtras(intent: android.content.Intent | null | undefined): { data: Record<string, string> } | null {
95
116
  try {
96
117
  const extras = intent?.getExtras?.();
97
118
  if (!extras || (!extras.containsKey('google.message_id') && !extras.containsKey('from'))) return null;
@@ -2,7 +2,8 @@
2
2
  * Minimal ambient declarations for the Firebase Messaging Android classes the FCM service extends.
3
3
  * The full classes are provided at runtime by the `firebase-messaging` AAR (added by the CLI only
4
4
  * when push is wired) — the NativeScript Android runtime marshals them. Declare-merges into the
5
- * `com` namespace from @nativescript/types-android. Mirrors ios-frameworks.d.ts.
5
+ * `com` namespace from @nativescript/types-android. (iOS SDK frameworks are typed via the
6
+ * @nativescript/types-ios references in references.d.ts.)
6
7
  */
7
8
  declare namespace com.google.firebase.messaging {
8
9
  class RemoteMessage {
@@ -2,7 +2,8 @@ import { Application, Utils } from '@nativescript/core';
2
2
  import { bridge } from './bridge';
3
3
  import { requestPermissions, startActivityForResult, uriToDataUrl, bitmapToDataUrl } from './android-helpers';
4
4
 
5
- declare const android: any, java: any, androidx: any;
5
+ // no NS types: android-32 typings omit ContactsContract/MediaStore column + ACTION_PICK_IMAGES constants this file reads
6
+ declare const android: any, androidx: any;
6
7
 
7
8
  const err = (code: string, message: string) => Object.assign(new Error(message), { code });
8
9
 
@@ -125,18 +126,21 @@ export function registerAndroidHandlers(): void {
125
126
  // ── geolocation (LocationManager) ──────────────────────────────────
126
127
  const GEO_PERMS = ['android.permission.ACCESS_FINE_LOCATION', 'android.permission.ACCESS_COARSE_LOCATION'];
127
128
  const locationManager = () => context().getSystemService(android.content.Context.LOCATION_SERVICE);
128
- const toPosition = (loc: any) => ({
129
+ const toPosition = (loc: android.location.Location) => ({
129
130
  lat: loc.getLatitude(), lng: loc.getLongitude(), accuracy: loc.getAccuracy(),
130
131
  });
131
- const makeListener = (onLocation: (loc: any) => void) =>
132
+ const makeListener = (onLocation: (loc: android.location.Location) => void) =>
132
133
  new android.location.LocationListener({
133
- // Both overloads dispatch here — newer Android may deliver a List<Location>
134
- onLocationChanged(loc: any) {
134
+ // Both overloads dispatch here — newer Android may deliver a List<Location>.
135
+ onLocationChanged(arg: android.location.Location | java.util.List<android.location.Location>) {
136
+ // interop: runtime duck-typing across the two Java overloads; TS can't narrow Java types here
137
+ let loc: any = arg;
135
138
  if (loc && typeof loc.getLatitude !== 'function' && typeof loc.size === 'function') {
136
139
  loc = loc.size() > 0 ? loc.get(loc.size() - 1) : null;
137
140
  }
138
141
  if (loc) onLocation(loc);
139
142
  },
143
+ onFlushComplete() {},
140
144
  onStatusChanged() {}, onProviderEnabled() {}, onProviderDisabled() {},
141
145
  });
142
146
 
@@ -148,7 +152,7 @@ export function registerAndroidHandlers(): void {
148
152
  ?? lm.getLastKnownLocation(android.location.LocationManager.NETWORK_PROVIDER);
149
153
  if (last) return resolve(toPosition(last));
150
154
 
151
- let listener: any = null;
155
+ let listener: android.location.LocationListener | null = null;
152
156
  const timer = setTimeout(() => {
153
157
  if (listener) lm.removeUpdates(listener);
154
158
  reject(err('TIMEOUT', 'location timeout'));
@@ -165,7 +169,7 @@ export function registerAndroidHandlers(): void {
165
169
  });
166
170
  });
167
171
 
168
- let geoWatchListener: any = null;
172
+ let geoWatchListener: android.location.LocationListener | null = null;
169
173
  bridge.register('geo.watch.start', async () => {
170
174
  if (geoWatchListener) return; // already streaming
171
175
  if (!(await requestPermissions(GEO_PERMS))) throw err('DENIED', 'location permission denied');
@@ -249,7 +253,7 @@ export function registerAndroidHandlers(): void {
249
253
 
250
254
  // ── motion (SensorManager: accelerometer + gyroscope) ──────────────
251
255
  const sensorManager = () => context().getSystemService(android.content.Context.SENSOR_SERVICE);
252
- let motionListener: any = null;
256
+ let motionListener: android.hardware.SensorEventListener | null = null;
253
257
  const lastMotion = { ax: 0, ay: 0, az: 0, rx: 0, ry: 0, rz: 0 };
254
258
 
255
259
  bridge.register('motion.start', () => {
@@ -261,7 +265,7 @@ export function registerAndroidHandlers(): void {
261
265
  let last = 0;
262
266
  motionListener = new android.hardware.SensorEventListener({
263
267
  onAccuracyChanged() {},
264
- onSensorChanged(event: any) {
268
+ onSensorChanged(event: android.hardware.SensorEvent) {
265
269
  const v = event.values;
266
270
  if (event.sensor.getType() === android.hardware.Sensor.TYPE_ACCELEROMETER) {
267
271
  // Android accelerometer already reports m/s² incl. gravity — kit contract matches.
@@ -296,7 +300,7 @@ export function registerAndroidHandlers(): void {
296
300
  if (resultCode !== android.app.Activity.RESULT_OK || !result?.getData()) return { picked: false };
297
301
 
298
302
  const cr = context().getContentResolver();
299
- const column = (uri: any, col: string, sel: string | null, args: string[] | null): string[] => {
303
+ const column = (uri: android.net.Uri, col: string, sel: string | null, args: string[] | null): string[] => {
300
304
  const out: string[] = [];
301
305
  const cursor = cr.query(uri, [col], sel, args, null);
302
306
  if (cursor) {
@@ -327,7 +331,7 @@ export function registerAndroidHandlers(): void {
327
331
  }
328
332
  const CC = android.provider.ContactsContract;
329
333
  const cr = context().getContentResolver();
330
- const column = (uri: any, col: string, sel: string | null, args: string[] | null): string[] => {
334
+ const column = (uri: android.net.Uri, col: string, sel: string | null, args: string[] | null): string[] => {
331
335
  const out: string[] = [];
332
336
  const cursor = cr.query(uri, [col], sel, args, null);
333
337
  if (cursor) {
@@ -415,7 +419,7 @@ export function registerAndroidHandlers(): void {
415
419
  dir.mkdirs();
416
420
  const uris = new java.util.ArrayList();
417
421
  const mimes = new java.util.HashSet();
418
- (files ?? []).forEach((f: any, i: number) => {
422
+ (files ?? []).forEach((f: { name: string; mimeType: string; base64: string }, i: number) => {
419
423
  const bytes = android.util.Base64.decode(f.base64, android.util.Base64.DEFAULT);
420
424
  const out = new java.io.File(dir, i + '-' + (f.name || 'file')); // index-prefix: avoid same-name collisions
421
425
  const fos = new java.io.FileOutputStream(out);