@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
@@ -5,6 +5,118 @@ import { onRemoteMessage } from './handlers-push';
5
5
  import { uiImageToDataUrl } from './ios-image';
6
6
  import { maskForLock, setIosOrientationMask } from './orientation';
7
7
 
8
+ interface GeoResult { lat: number; lng: number; accuracy: number; }
9
+
10
+ // CLLocationManager delegate. Closure-captured callbacks (resolve/reject/cleanup) are assigned as
11
+ // instance fields after new() — same pattern as handlers-speech/oauth.
12
+ @NativeClass()
13
+ class GeoDelegate extends NSObject implements CLLocationManagerDelegate {
14
+ static ObjCProtocols = [CLLocationManagerDelegate];
15
+ onResult?: (r: GeoResult) => void;
16
+ onError?: (e: Error) => void;
17
+ onCleanup?: () => void;
18
+ static new(): GeoDelegate {
19
+ return <GeoDelegate>super.new();
20
+ }
21
+ locationManagerDidUpdateLocations(_m: CLLocationManager, locations: NSArray<CLLocation>): void {
22
+ const loc = locations.lastObject;
23
+ if (!loc) return;
24
+ const result = {
25
+ lat: loc.coordinate.latitude,
26
+ lng: loc.coordinate.longitude,
27
+ accuracy: loc.horizontalAccuracy,
28
+ };
29
+ this.onCleanup?.();
30
+ this.onResult?.(result);
31
+ }
32
+ locationManagerDidFailWithError(_m: CLLocationManager, error: NSError): void {
33
+ this.onCleanup?.();
34
+ this.onError?.(Object.assign(new Error(error.localizedDescription), { code: 'DENIED' }));
35
+ }
36
+ locationManagerDidChangeAuthorization(m: CLLocationManager): void {
37
+ const st = m.authorizationStatus;
38
+ if (st === CLAuthorizationStatus.kCLAuthorizationStatusAuthorizedWhenInUse ||
39
+ st === CLAuthorizationStatus.kCLAuthorizationStatusAuthorizedAlways) {
40
+ m.startUpdatingLocation();
41
+ } else if (st === CLAuthorizationStatus.kCLAuthorizationStatusDenied) {
42
+ this.onCleanup?.();
43
+ this.onError?.(Object.assign(new Error('location permission denied'), { code: 'DENIED' }));
44
+ }
45
+ }
46
+ }
47
+
48
+ // PHPickerViewController delegate. Closure-captured opts (dataUrl/maxSize) + resolve assigned post-new().
49
+ @NativeClass()
50
+ class PhotoPickerDelegate extends NSObject implements PHPickerViewControllerDelegate {
51
+ static ObjCProtocols = [PHPickerViewControllerDelegate];
52
+ wantDataUrl?: boolean;
53
+ maxSize?: number;
54
+ onResult?: (r: { picked: boolean; width?: number; height?: number; dataUrl?: string }) => void;
55
+ static new(): PhotoPickerDelegate {
56
+ return <PhotoPickerDelegate>super.new();
57
+ }
58
+ pickerDidFinishPicking(picker: PHPickerViewController, results: NSArray<PHPickerResult>): void {
59
+ picker.dismissViewControllerAnimatedCompletion(true, null);
60
+ const result = results.count > 0 ? results.objectAtIndex(0) : null;
61
+ if (!result) return this.onResult?.({ picked: false });
62
+ result.itemProvider.loadObjectOfClassCompletionHandler(UIImage.class(), (img: UIImage) => {
63
+ if (!img) return this.onResult?.({ picked: true });
64
+ const out: { picked: boolean; width?: number; height?: number; dataUrl?: string } =
65
+ { picked: true, width: img.size.width, height: img.size.height };
66
+ if (this.wantDataUrl) out.dataUrl = uiImageToDataUrl(img, this.maxSize ?? 1024);
67
+ this.onResult?.(out);
68
+ });
69
+ }
70
+ }
71
+
72
+ // UNUserNotificationCenter delegate — foreground present + tap routing. Idempotent singleton (notifDelegate).
73
+ @NativeClass()
74
+ class ForegroundNotificationDelegate extends NSObject implements UNUserNotificationCenterDelegate {
75
+ static ObjCProtocols = [UNUserNotificationCenterDelegate];
76
+ static new(): ForegroundNotificationDelegate {
77
+ return <ForegroundNotificationDelegate>super.new();
78
+ }
79
+ userNotificationCenterWillPresentNotificationWithCompletionHandler(
80
+ _center: UNUserNotificationCenter,
81
+ notification: UNNotification,
82
+ completionHandler: (opts: UNNotificationPresentationOptions) => void
83
+ ): void {
84
+ // Foreground REMOTE push (push trigger) → surface to kit.push.onMessage.
85
+ if (isRemotePush(notification)) onRemoteMessage(notification.request.content.userInfo, false);
86
+ // Present banner + list + sound + badge even while foreground (min target iOS 16).
87
+ completionHandler(
88
+ UNNotificationPresentationOptions.Banner |
89
+ UNNotificationPresentationOptions.List |
90
+ UNNotificationPresentationOptions.Sound |
91
+ UNNotificationPresentationOptions.Badge
92
+ );
93
+ }
94
+ // Notification tapped → route its deep link through the same path as an external open.
95
+ userNotificationCenterDidReceiveNotificationResponseWithCompletionHandler(
96
+ _center: UNUserNotificationCenter,
97
+ response: UNNotificationResponse,
98
+ completionHandler: () => void
99
+ ): void {
100
+ // Tapped REMOTE push → kit.push.onTap (with payload). Local notifs keep the deep-link path below.
101
+ if (isRemotePush(response.notification)) onRemoteMessage(response.notification.request.content.userInfo, true);
102
+ // userInfo may come back as an NSDictionary or an auto-marshalled JS object.
103
+ // any: dual-path payload (NSDictionary vs marshalled JS object) probed dynamically.
104
+ const info: any = response.notification.request.content.userInfo;
105
+ const url = info ? (typeof info.objectForKey === 'function' ? info.objectForKey('url') : info.url) : null;
106
+ // Diagnostic breadcrumb (persists across the cold relaunch) — surfaced in the handshake's debug field.
107
+ try {
108
+ ApplicationSettings.setString(
109
+ 'kit:__notifTap',
110
+ JSON.stringify({ at: Date.now(), url: url ? String(url) : null, hadInfo: !!info })
111
+ );
112
+ } catch {
113
+ /* diagnostic only */
114
+ }
115
+ if (url) onDeepLink(String(url));
116
+ completionHandler();
117
+ }
118
+ }
119
+
8
120
  /**
9
121
  * Extended capability handlers — iOS-first (Android arrives with the transport upgrade).
10
122
  * Each handler is small and self-contained; heavy domains (push, files) come later.
@@ -62,11 +174,11 @@ export function registerExtendedHandlers(): void {
62
174
  bridge.register('storage.secure.get', async ({ key }: { key: string }) =>
63
175
  (await secure.get({ key: `kit:${key}` })) ?? null
64
176
  );
65
- bridge.register('storage.secure.set', ({ key, value }: { key: string; value: string }) =>
66
- secure.set({ key: `kit:${key}`, value: String(value ?? '') }).then(() => undefined)
177
+ bridge.register('storage.secure.set', ({ key, value }: { key: string; value: string }): Promise<void> =>
178
+ secure.set({ key: `kit:${key}`, value: String(value ?? '') }).then((): void => undefined)
67
179
  );
68
- bridge.register('storage.secure.remove', ({ key }: { key: string }) =>
69
- secure.remove({ key: `kit:${key}` }).then(() => undefined)
180
+ bridge.register('storage.secure.remove', ({ key }: { key: string }): Promise<void> =>
181
+ secure.remove({ key: `kit:${key}` }).then((): void => undefined)
70
182
  );
71
183
 
72
184
  // ── notifications (UNUserNotificationCenter, iOS) ──────────────────
@@ -85,7 +197,7 @@ export function registerExtendedHandlers(): void {
85
197
  });
86
198
  });
87
199
 
88
- bridge.register('notifications.schedule', ({ id, title, body, delaySec, deepLink }: any) => {
200
+ bridge.register('notifications.schedule', ({ id, title, body, delaySec, deepLink }: { id?: number; title?: string; body?: string; delaySec?: number; deepLink?: string }) => {
89
201
  if (!isIOS) throw Object.assign(new Error('iOS only for now'), { code: 'UNSUPPORTED' });
90
202
  const nid = id ?? Math.floor(Math.random() * 100000);
91
203
  return new Promise((resolve, reject) => {
@@ -95,6 +207,7 @@ export function registerExtendedHandlers(): void {
95
207
  // Carry the deep-link on the notification; the tap delegate re-emits it.
96
208
  // Plain JS object → NativeScript marshals it to NSDictionary (more reliable
97
209
  // than dictionaryWithObjectForKey across NS versions).
210
+ // cast: NS marshals a plain JS object → NSDictionary at the interop boundary (typed NSDictionary).
98
211
  if (deepLink) content.userInfo = { url: String(deepLink) } as any;
99
212
  const trigger = UNTimeIntervalNotificationTrigger.triggerWithTimeIntervalRepeats(
100
213
  Math.max(1, delaySec ?? 1),
@@ -130,7 +243,7 @@ export function registerExtendedHandlers(): void {
130
243
  // iOS silently ignores the badge unless notification authorization (incl. .badge) was granted —
131
244
  // the completion's error is the ONLY signal, so DON'T swallow it (else the badge just never
132
245
  // appears with no explanation). Resolve regardless (the JS call itself didn't fail).
133
- (error: any) => { if (error) console.warn('[appwrap] setBadge ignored — notifications not authorized?', String(error)); }
246
+ (error: NSError) => { if (error) console.warn('[appwrap] setBadge ignored — notifications not authorized?', String(error)); }
134
247
  );
135
248
  resolve();
136
249
  });
@@ -152,8 +265,8 @@ export function registerExtendedHandlers(): void {
152
265
  LAPolicy.DeviceOwnerAuthenticationWithBiometrics
153
266
  );
154
267
  const type =
155
- ctx.biometryType === LABiometryType.FaceID ? 'face'
156
- : ctx.biometryType === LABiometryType.TouchID ? 'touch'
268
+ ctx.biometryType === LABiometryType.TypeFaceID ? 'face'
269
+ : ctx.biometryType === LABiometryType.TypeTouchID ? 'touch'
157
270
  : 'none';
158
271
  return { available, type };
159
272
  });
@@ -183,7 +296,7 @@ export function registerExtendedHandlers(): void {
183
296
  }, 15000);
184
297
 
185
298
  let manager: CLLocationManager | null = null;
186
- let delegate: any = null;
299
+ let delegate: GeoDelegate | null = null;
187
300
  const cleanup = () => {
188
301
  clearTimeout(timer);
189
302
  manager?.stopUpdatingLocation();
@@ -191,40 +304,12 @@ export function registerExtendedHandlers(): void {
191
304
  delegate = null;
192
305
  };
193
306
 
194
- const DelegateClass = (NSObject as any).extend(
195
- {
196
- locationManagerDidUpdateLocations(_m: CLLocationManager, locations: NSArray<CLLocation>) {
197
- const loc = locations.lastObject;
198
- if (!loc) return;
199
- const result = {
200
- lat: loc.coordinate.latitude,
201
- lng: loc.coordinate.longitude,
202
- accuracy: loc.horizontalAccuracy,
203
- };
204
- cleanup();
205
- resolve(result);
206
- },
207
- locationManagerDidFailWithError(_m: CLLocationManager, error: NSError) {
208
- cleanup();
209
- reject(Object.assign(new Error(error.localizedDescription), { code: 'DENIED' }));
210
- },
211
- locationManagerDidChangeAuthorization(m: CLLocationManager) {
212
- const st = m.authorizationStatus;
213
- if (st === CLAuthorizationStatus.kCLAuthorizationStatusAuthorizedWhenInUse ||
214
- st === CLAuthorizationStatus.kCLAuthorizationStatusAuthorizedAlways) {
215
- m.startUpdatingLocation();
216
- } else if (st === CLAuthorizationStatus.kCLAuthorizationStatusDenied) {
217
- cleanup();
218
- reject(Object.assign(new Error('location permission denied'), { code: 'DENIED' }));
219
- }
220
- },
221
- },
222
- { protocols: [CLLocationManagerDelegate] }
223
- );
224
-
225
307
  Utils.dispatchToMainThread(() => {
226
308
  manager = CLLocationManager.new();
227
- delegate = DelegateClass.new();
309
+ delegate = GeoDelegate.new();
310
+ delegate.onResult = resolve;
311
+ delegate.onError = reject;
312
+ delegate.onCleanup = cleanup;
228
313
  manager.delegate = delegate;
229
314
  manager.desiredAccuracy = kCLLocationAccuracyHundredMeters;
230
315
  const st = manager.authorizationStatus;
@@ -238,31 +323,18 @@ export function registerExtendedHandlers(): void {
238
323
  });
239
324
 
240
325
  // ── photos (PHPickerViewController) ────────────────────────────────
241
- bridge.register('photos.pick', ({ dataUrl, maxSize }: any = {}) => {
326
+ bridge.register('photos.pick', ({ dataUrl, maxSize }: { dataUrl?: boolean; maxSize?: number } = {}) => {
242
327
  if (!isIOS) throw Object.assign(new Error('iOS only for now'), { code: 'UNSUPPORTED' });
243
328
  return new Promise((resolve) => {
244
329
  Utils.dispatchToMainThread(() => {
245
330
  const config = PHPickerConfiguration.new();
246
331
  config.selectionLimit = 1;
247
- const DelegateClass = (NSObject as any).extend(
248
- {
249
- pickerDidFinishPicking(picker: PHPickerViewController, results: NSArray<PHPickerResult>) {
250
- picker.dismissViewControllerAnimatedCompletion(true, null);
251
- const result = results.count > 0 ? results.objectAtIndex(0) : null;
252
- if (!result) return resolve({ picked: false });
253
- result.itemProvider.loadObjectOfClassCompletionHandler(UIImage.class(), (img: UIImage) => {
254
- if (!img) return resolve({ picked: true });
255
- const out: any = { picked: true, width: img.size.width, height: img.size.height };
256
- if (dataUrl) out.dataUrl = uiImageToDataUrl(img, maxSize ?? 1024);
257
- resolve(out);
258
- });
259
- },
260
- },
261
- { protocols: [PHPickerViewControllerDelegate] }
262
- );
263
332
  const picker = PHPickerViewController.alloc().initWithConfiguration(config);
264
- const delegate = DelegateClass.new();
265
- (picker as any)._appwrapDelegate = delegate; // retain
333
+ const delegate = PhotoPickerDelegate.new();
334
+ delegate.wantDataUrl = !!dataUrl;
335
+ delegate.maxSize = maxSize;
336
+ delegate.onResult = resolve;
337
+ (picker as any)._appwrapDelegate = delegate; // any: stash extra prop on VC to retain delegate (ARC)
266
338
  picker.delegate = delegate;
267
339
  Utils.ios.getRootViewController().presentViewControllerAnimatedCompletion(picker, true, null);
268
340
  });
@@ -314,14 +386,16 @@ export function registerExtendedHandlers(): void {
314
386
  function applyIosOrientation(mask: number): void {
315
387
  setIosOrientationMask(mask);
316
388
  Utils.dispatchToMainThread(() => {
317
- const rootVC: any = Utils.ios.getRootViewController();
389
+ const rootVC = Utils.ios.getRootViewController();
318
390
  rootVC?.setNeedsUpdateOfSupportedInterfaceOrientations?.();
319
- const G: any = global as any;
320
- const Prefs = G.UIWindowSceneGeometryPreferencesIOS;
321
- const scenes: any = UIApplication.sharedApplication.connectedScenes?.allObjects;
391
+ // any: deliberate runtime probe — reference the iOS-16 geometry class via `global` so PRE-iOS-16
392
+ // runtimes (where it's absent) hit the early-return instead of a hard symbol miss.
393
+ const Prefs: any = (global as any).UIWindowSceneGeometryPreferencesIOS;
394
+ const scenes = UIApplication.sharedApplication.connectedScenes?.allObjects;
322
395
  if (!Prefs || !scenes) return; // pre-iOS-16: the mask alone takes effect on next rotation
323
396
  for (let i = 0; i < scenes.count; i++) {
324
- const scene = scenes.objectAtIndex(i);
397
+ // any: ObjC errorHandler block is nullable — we pass `null`, which the typed (p1)=>void rejects.
398
+ const scene: any = scenes.objectAtIndex(i);
325
399
  if (scene?.requestGeometryUpdateWithPreferencesErrorHandler) {
326
400
  const prefs = Prefs.alloc().initWithInterfaceOrientations(mask);
327
401
  scene.requestGeometryUpdateWithPreferencesErrorHandler(prefs, null);
@@ -330,7 +404,7 @@ function applyIosOrientation(mask: number): void {
330
404
  });
331
405
  }
332
406
 
333
- let notifDelegate: any = null; // retained for the app's lifetime
407
+ let notifDelegate: ForegroundNotificationDelegate | null = null; // retained for the app's lifetime
334
408
  /**
335
409
  * Install the UNUserNotificationCenter delegate. MUST run before
336
410
  * `didFinishLaunching` returns, else a tap that COLD-LAUNCHES the app never
@@ -340,50 +414,7 @@ let notifDelegate: any = null; // retained for the app's lifetime
340
414
  */
341
415
  export function installForegroundNotificationDelegate(): void {
342
416
  if (!isIOS || notifDelegate) return;
343
- const DelegateClass = (NSObject as any).extend(
344
- {
345
- userNotificationCenterWillPresentNotificationWithCompletionHandler(
346
- _center: UNUserNotificationCenter,
347
- notification: UNNotification,
348
- completionHandler: (opts: number) => void
349
- ): void {
350
- // Foreground REMOTE push (push trigger) → surface to kit.push.onMessage.
351
- if (isRemotePush(notification)) onRemoteMessage(notification.request.content.userInfo, false);
352
- // Present banner + list + sound + badge even while foreground (min target iOS 16).
353
- completionHandler(
354
- UNNotificationPresentationOptions.Banner |
355
- UNNotificationPresentationOptions.List |
356
- UNNotificationPresentationOptions.Sound |
357
- UNNotificationPresentationOptions.Badge
358
- );
359
- },
360
- // Notification tapped → route its deep link through the same path as an external open.
361
- userNotificationCenterDidReceiveNotificationResponseWithCompletionHandler(
362
- _center: UNUserNotificationCenter,
363
- response: UNNotificationResponse,
364
- completionHandler: () => void
365
- ): void {
366
- // Tapped REMOTE push → kit.push.onTap (with payload). Local notifs keep the deep-link path below.
367
- if (isRemotePush(response.notification)) onRemoteMessage(response.notification.request.content.userInfo, true);
368
- // userInfo may come back as an NSDictionary or an auto-marshalled JS object.
369
- const info: any = response.notification.request.content.userInfo;
370
- const url = info ? (typeof info.objectForKey === 'function' ? info.objectForKey('url') : info.url) : null;
371
- // Diagnostic breadcrumb (persists across the cold relaunch) — surfaced in the handshake's debug field.
372
- try {
373
- ApplicationSettings.setString(
374
- 'kit:__notifTap',
375
- JSON.stringify({ at: Date.now(), url: url ? String(url) : null, hadInfo: !!info })
376
- );
377
- } catch {
378
- /* diagnostic only */
379
- }
380
- if (url) onDeepLink(String(url));
381
- completionHandler();
382
- },
383
- },
384
- { protocols: [UNUserNotificationCenterDelegate] }
385
- );
386
- notifDelegate = DelegateClass.new();
417
+ notifDelegate = ForegroundNotificationDelegate.new();
387
418
  UNUserNotificationCenter.currentNotificationCenter().delegate = notifDelegate;
388
419
  }
389
420
 
@@ -2,8 +2,6 @@ import { Utils, isAndroid, isIOS } from '@nativescript/core';
2
2
  import { bridge } from './bridge';
3
3
  import { startActivityForResult } from './android-helpers';
4
4
 
5
- declare const android: any, java: any;
6
-
7
5
  const err = (code: string, message: string) => Object.assign(new Error(message), { code });
8
6
 
9
7
  type Dir = 'documents' | 'data' | 'cache';
@@ -152,6 +150,46 @@ function iosStat(full: string, name: string): any {
152
150
  };
153
151
  }
154
152
 
153
+ /**
154
+ * Delegate for UIDocumentPickerViewController. Captures the resolve callback as an instance field
155
+ * (the original closed over `resolve`); both selectors funnel into it.
156
+ */
157
+ @NativeClass()
158
+ class DocumentPickerDelegate extends NSObject implements UIDocumentPickerDelegate {
159
+ static ObjCProtocols = [UIDocumentPickerDelegate];
160
+ onPick!: (out: any[]) => void;
161
+
162
+ static new(): DocumentPickerDelegate {
163
+ return <DocumentPickerDelegate>super.new();
164
+ }
165
+
166
+ documentPickerDidPickDocumentsAtURLs(_controller: UIDocumentPickerViewController, urls: NSArray<NSURL>): void {
167
+ const out: any[] = [];
168
+ for (let i = 0; i < urls.count; i++) {
169
+ const url = urls.objectAtIndex(i);
170
+ const scoped = url.startAccessingSecurityScopedResource?.();
171
+ try {
172
+ const data = NSData.dataWithContentsOfURL(url);
173
+ if (data) {
174
+ out.push({
175
+ name: url.lastPathComponent,
176
+ mimeType: mimeForUrlIos(url),
177
+ size: data.length,
178
+ base64: data.base64EncodedStringWithOptions(0 as unknown as NSDataBase64EncodingOptions),
179
+ });
180
+ }
181
+ } finally {
182
+ if (scoped) url.stopAccessingSecurityScopedResource?.();
183
+ }
184
+ }
185
+ this.onPick(out);
186
+ }
187
+
188
+ documentPickerWasCancelled(_controller: UIDocumentPickerViewController): void {
189
+ this.onPick([]);
190
+ }
191
+ }
192
+
155
193
  /**
156
194
  * iOS document picker via UIDocumentPickerViewController. Reads each chosen URL's bytes (under a
157
195
  * security-scoped access window) and returns them base64-inline. DEVICE-GATED — the delegate
@@ -163,6 +201,7 @@ function pickFileIos(types: string[] | undefined, multiple: boolean): Promise<an
163
201
  try {
164
202
  const utis = NSMutableArray.new();
165
203
  // Map common MIME/extension hints to UTTypes; default to "any item".
204
+ // interop: UTType/UTTypeItem live in UniformTypeIdentifiers, not referenced by references.d.ts.
166
205
  (types ?? []).forEach((t) => {
167
206
  const ut = (UTType as any).typeWithMIMEType?.(t) ?? (UTType as any).typeWithFilenameExtension?.(t.replace(/^\./, ''));
168
207
  if (ut) utis.addObject(ut);
@@ -172,37 +211,9 @@ function pickFileIos(types: string[] | undefined, multiple: boolean): Promise<an
172
211
  const picker = UIDocumentPickerViewController.alloc().initForOpeningContentTypes(utis as any);
173
212
  picker.allowsMultipleSelection = multiple;
174
213
 
175
- const DelegateClass = (NSObject as any).extend(
176
- {
177
- documentPickerDidPickDocumentsAtURLs(_p: any, urls: any) {
178
- const out: any[] = [];
179
- for (let i = 0; i < urls.count; i++) {
180
- const url = urls.objectAtIndex(i);
181
- const scoped = url.startAccessingSecurityScopedResource?.();
182
- try {
183
- const data = NSData.dataWithContentsOfURL(url);
184
- if (data) {
185
- out.push({
186
- name: url.lastPathComponent,
187
- mimeType: mimeForUrlIos(url),
188
- size: data.length,
189
- base64: data.base64EncodedStringWithOptions(0 as unknown as NSDataBase64EncodingOptions),
190
- });
191
- }
192
- } finally {
193
- if (scoped) url.stopAccessingSecurityScopedResource?.();
194
- }
195
- }
196
- resolve(out);
197
- },
198
- documentPickerWasCancelled() {
199
- resolve([]);
200
- },
201
- },
202
- { protocols: [UIDocumentPickerDelegate] }
203
- );
204
- const delegate = DelegateClass.new();
205
- (picker as any)._appwrapDelegate = delegate; // retain past the present call
214
+ const delegate = DocumentPickerDelegate.new();
215
+ delegate.onPick = resolve;
216
+ (picker as any)._appwrapDelegate = delegate; // interop: retain delegate past the present call (no typed slot)
206
217
  picker.delegate = delegate;
207
218
  Utils.ios.getRootViewController().presentViewControllerAnimatedCompletion(picker, true, null);
208
219
  } catch (e: any) {
@@ -212,25 +223,26 @@ function pickFileIos(types: string[] | undefined, multiple: boolean): Promise<an
212
223
  });
213
224
  }
214
225
 
215
- function mimeForUrlIos(url: any): string {
226
+ function mimeForUrlIos(url: NSURL): string {
227
+ // interop: UTType lives in UniformTypeIdentifiers, not referenced by references.d.ts.
216
228
  const ut = (UTType as any).typeWithFilenameExtension?.(url.pathExtension);
217
229
  return ut?.preferredMIMEType ?? 'application/octet-stream';
218
230
  }
219
231
 
220
232
  // ───────────────────────────── Android ─────────────────────────────
221
233
 
222
- function androidBaseDir(dir: Dir): any {
234
+ function androidBaseDir(dir: Dir): java.io.File {
223
235
  const ctx = Utils.android.getApplicationContext();
224
236
  if (dir === 'cache') return ctx.getCacheDir();
225
237
  if (dir === 'documents') return ctx.getExternalFilesDir(null) ?? ctx.getFilesDir();
226
238
  return ctx.getFilesDir(); // 'data'
227
239
  }
228
240
 
229
- function androidFile(path: string, dir: Dir): any {
241
+ function androidFile(path: string, dir: Dir): java.io.File {
230
242
  return new java.io.File(androidBaseDir(dir), safeRelPath(path));
231
243
  }
232
244
 
233
- function readBytesAndroid(file: any): any {
245
+ function readBytesAndroid(file: java.io.File): androidNative.Array<number> {
234
246
  const fis = new java.io.FileInputStream(file);
235
247
  const baos = new java.io.ByteArrayOutputStream();
236
248
  const buf = Array.create('byte', 8192);
@@ -298,7 +310,7 @@ function registerAndroidFs(): void {
298
310
  bridge.register('fs.pickFile', ({ types, multiple }: any) => pickFileAndroid(types, !!multiple));
299
311
  }
300
312
 
301
- function writeAndroid(file: any, data: string, encoding: string, append: boolean): void {
313
+ function writeAndroid(file: java.io.File, data: string, encoding: string, append: boolean): void {
302
314
  const bytes = encoding === 'base64'
303
315
  ? android.util.Base64.decode(String(data ?? ''), android.util.Base64.DEFAULT)
304
316
  : new java.lang.String(String(data ?? '')).getBytes('UTF-8');
@@ -307,7 +319,7 @@ function writeAndroid(file: any, data: string, encoding: string, append: boolean
307
319
  fos.close();
308
320
  }
309
321
 
310
- function androidStat(file: any, name: string): any {
322
+ function androidStat(file: java.io.File, name: string): any {
311
323
  const isDir = file.isDirectory();
312
324
  return {
313
325
  name,
@@ -338,7 +350,7 @@ async function pickFileAndroid(types: string[] | undefined, multiple: boolean):
338
350
  const { resultCode, intent: result } = await startActivityForResult(intent);
339
351
  if (resultCode !== android.app.Activity.RESULT_OK || !result) return [];
340
352
 
341
- const uris: any[] = [];
353
+ const uris: android.net.Uri[] = [];
342
354
  const clip = result.getClipData?.();
343
355
  if (clip) for (let i = 0; i < clip.getItemCount(); i++) uris.push(clip.getItemAt(i).getUri());
344
356
  else if (result.getData()) uris.push(result.getData());
@@ -361,7 +373,7 @@ async function pickFileAndroid(types: string[] | undefined, multiple: boolean):
361
373
  });
362
374
  }
363
375
 
364
- function queryDisplayName(cr: any, uri: any): string {
376
+ function queryDisplayName(cr: android.content.ContentResolver, uri: android.net.Uri): string {
365
377
  const cursor = cr.query(uri, [android.provider.OpenableColumns.DISPLAY_NAME], null, null, null);
366
378
  let name = 'file';
367
379
  if (cursor) {
@@ -2,9 +2,7 @@ import { Utils, isAndroid, isIOS } from '@nativescript/core';
2
2
  import { bridge } from './bridge';
3
3
  import { requestPermissions, startActivityForResult } from './android-helpers';
4
4
 
5
- declare const android: any;
6
- declare const cc: any; // generated from the HealthConnectBridge.kt shim (overrides/)
7
- declare const CMPedometer: any; // CoreMotion (already linked via CMMotionManager in handlers-parity)
5
+ declare const cc: any; // no NS types: generated Kotlin HealthConnectBridge (HealthConnectBridge.kt shim, overrides/)
8
6
 
9
7
  const err = (code: string, message: string) => Object.assign(new Error(message), { code });
10
8
 
@@ -39,7 +37,8 @@ function registerIos(): void {
39
37
  new Promise((resolve) => {
40
38
  if (!HKHealthStore.isHealthDataAvailable()) { resolve(false); return; }
41
39
  const readSet = NSSet.setWithObject(stepType);
42
- store.requestAuthorizationToShareTypesReadTypesCompletion(null, readSet, (ok: boolean, e: any) => {
40
+ // null typesToShare (read-only request) — SDK types the param as a non-nullable NSSet.
41
+ store.requestAuthorizationToShareTypesReadTypesCompletion(null as any, readSet, (ok: boolean, e: NSError) => {
43
42
  Utils.dispatchToMainThread(() => resolve(!!ok && !e));
44
43
  });
45
44
  })
@@ -54,9 +53,12 @@ function registerIos(): void {
54
53
  // WKWebView response delivery (evaluateJavaScript) is main-thread.
55
54
  bridge.register('health.count', () =>
56
55
  new Promise((resolve, reject) => {
57
- const predicate = HKQuery.predicateForSamplesWithStartDateEndDateOptions(startOfToday(), NSDate.date(), 0);
56
+ // NS marshals NSDate→the SDK's `Date` param type; options 0 = HKQueryOptions.None.
57
+ const predicate = HKQuery.predicateForSamplesWithStartDateEndDateOptions(
58
+ startOfToday() as unknown as Date, NSDate.date() as unknown as Date, HKQueryOptions.None
59
+ );
58
60
  const q = HKStatisticsQuery.alloc().initWithQuantityTypeQuantitySamplePredicateOptionsCompletionHandler(
59
- stepType, predicate, HKStatisticsOptions.CumulativeSum, (_q: any, stats: any, e: any) => {
61
+ stepType, predicate, HKStatisticsOptions.CumulativeSum, (_q: HKStatisticsQuery, stats: HKStatistics, e: NSError) => {
60
62
  Utils.dispatchToMainThread(() => {
61
63
  if (e) { reject(err('NATIVE_ERROR', e.localizedDescription ?? 'health query failed')); return; }
62
64
  const sum = stats && stats.sumQuantity ? stats.sumQuantity() : null;
@@ -72,17 +74,19 @@ function registerIos(): void {
72
74
  // aggregate. Caller uses HealthKit as the periodic source-of-truth and these live deltas in between.
73
75
  // Emits `health.liveStep` { steps } (today's pedometer total since local midnight). iPhone-only
74
76
  // (no Watch), which is fine: it's the live driver; HealthKit re-anchors the authoritative total.
75
- let pedometer: any = null;
77
+ let pedometer: CMPedometer | null = null;
76
78
  let anchorDay = -1; // local day-of-month the current stream is anchored to (for midnight re-anchor)
77
79
  const localDay = () => new Date().getDate();
78
80
  const startStream = () => {
81
+ if (!pedometer) return;
79
82
  anchorDay = localDay();
80
- pedometer.startPedometerUpdatesFromDateWithHandler(startOfToday(), (data: any, e: any) => {
83
+ // startOfToday() returns an NSDate; the SDK types `start` as JS Date but NativeScript marshals it.
84
+ pedometer.startPedometerUpdatesFromDateWithHandler(startOfToday() as unknown as Date, (data: CMPedometerData, e: NSError) => {
81
85
  if (e || !data) return;
82
86
  // Midnight self-heal: CMPedometer counts since the FIXED anchor date, so after local midnight
83
87
  // numberOfSteps still includes yesterday. When the day rolls over, restart from the new
84
88
  // midnight (re-anchors to 0) so the live count stays "today only".
85
- if (localDay() !== anchorDay) { Utils.dispatchToMainThread(() => { pedometer.stopPedometerUpdates(); startStream(); bridge.emit('health.liveStep', { steps: 0 }); }); return; }
89
+ if (localDay() !== anchorDay) { Utils.dispatchToMainThread(() => { pedometer?.stopPedometerUpdates(); startStream(); bridge.emit('health.liveStep', { steps: 0 }); }); return; }
86
90
  const steps = Math.round(num(data.numberOfSteps));
87
91
  Utils.dispatchToMainThread(() => bridge.emit('health.liveStep', { steps }));
88
92
  });
@@ -91,9 +95,12 @@ function registerIos(): void {
91
95
  if (typeof CMPedometer === 'undefined' || !CMPedometer.isStepCountingAvailable())
92
96
  throw err('UNSUPPORTED', 'Pedometer (live step counting) unavailable on this device');
93
97
  // Motion & Fitness denied/restricted → surface it so the caller can fall back to HealthKit instead
94
- // of silently receiving no events. (2 = denied, 1 = restricted per CMAuthorizationStatus.)
95
- const auth = typeof CMPedometer.authorizationStatus === 'function' ? CMPedometer.authorizationStatus() : 0;
96
- if (auth === 2 || auth === 1) throw err('DENIED', 'Motion & Fitness access is off for live step counting');
98
+ // of silently receiving no events.
99
+ const auth = typeof CMPedometer.authorizationStatus === 'function'
100
+ ? CMPedometer.authorizationStatus()
101
+ : CMAuthorizationStatus.NotDetermined;
102
+ if (auth === CMAuthorizationStatus.Denied || auth === CMAuthorizationStatus.Restricted)
103
+ throw err('DENIED', 'Motion & Fitness access is off for live step counting');
97
104
  if (pedometer) return; // already streaming
98
105
  pedometer = CMPedometer.new();
99
106
  startStream();
@@ -117,29 +124,30 @@ function registerAndroid(): void {
117
124
  };
118
125
 
119
126
  // ── sensor fallback (TYPE_STEP_COUNTER) — when Health Connect isn't available ──
120
- let sensorManager: any = null;
121
- let listener: any = null;
127
+ let sensorManager: android.hardware.SensorManager | null = null;
128
+ let listener: android.hardware.SensorEventListener | null = null;
122
129
  let baseline: number | null = null;
123
130
  let latest: number | null = null;
124
131
  let registered = false;
125
132
  const ensureSensor = () => {
126
- if (!sensorManager) sensorManager = ctx().getSystemService(android.content.Context.SENSOR_SERVICE);
133
+ // getSystemService is typed `any` by the SDK; narrow to the concrete manager.
134
+ if (!sensorManager) sensorManager = ctx().getSystemService(android.content.Context.SENSOR_SERVICE) as android.hardware.SensorManager;
127
135
  };
128
136
  const stopSensor = () => {
129
- if (registered) { sensorManager.unregisterListener(listener); registered = false; }
137
+ if (registered && sensorManager && listener) { sensorManager.unregisterListener(listener); registered = false; }
130
138
  };
131
139
  const startSensor = () => {
132
140
  ensureSensor();
133
- const sensor = sensorManager.getDefaultSensor(android.hardware.Sensor.TYPE_STEP_COUNTER);
141
+ const sensor = sensorManager!.getDefaultSensor(android.hardware.Sensor.TYPE_STEP_COUNTER);
134
142
  if (!sensor) throw err('UNSUPPORTED', 'No step-counter sensor on this device');
135
143
  stopSensor();
136
144
  baseline = null;
137
145
  latest = null;
138
146
  listener = new android.hardware.SensorEventListener({
139
- onSensorChanged: (ev: any) => { const v = num(ev.values[0]); latest = v; if (baseline == null) baseline = v; },
147
+ onSensorChanged: (ev: android.hardware.SensorEvent) => { const v = num(ev.values[0]); latest = v; if (baseline == null) baseline = v; },
140
148
  onAccuracyChanged: () => {},
141
149
  });
142
- sensorManager.registerListener(listener, sensor, android.hardware.SensorManager.SENSOR_DELAY_NORMAL);
150
+ sensorManager!.registerListener(listener, sensor, android.hardware.SensorManager.SENSOR_DELAY_NORMAL);
143
151
  registered = true;
144
152
  };
145
153
  const sensorCount = () => ({ steps: baseline != null && latest != null ? Math.max(0, Math.round(latest - baseline)) : 0 });