@livx.cc/appwrap 0.28.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 (34) hide show
  1. package/package.json +1 -1
  2. package/runtime/app/app.ts +55 -54
  3. package/runtime/app/shell/android-helpers.ts +6 -6
  4. package/runtime/app/shell/banner.ts +9 -4
  5. package/runtime/app/shell/bridge.ts +2 -0
  6. package/runtime/app/shell/custom-webview.android.ts +50 -23
  7. package/runtime/app/shell/custom-webview.ios.ts +90 -78
  8. package/runtime/app/shell/devmenu.ts +2 -2
  9. package/runtime/app/shell/env.ts +1 -3
  10. package/runtime/app/shell/events.ts +5 -4
  11. package/runtime/app/shell/handlers-android.ts +16 -12
  12. package/runtime/app/shell/handlers-background.ts +47 -36
  13. package/runtime/app/shell/handlers-billing.ts +90 -82
  14. package/runtime/app/shell/handlers-extended.ts +141 -110
  15. package/runtime/app/shell/handlers-fs.ts +53 -41
  16. package/runtime/app/shell/handlers-health.ts +27 -19
  17. package/runtime/app/shell/handlers-media.ts +8 -14
  18. package/runtime/app/shell/handlers-oauth.ts +19 -19
  19. package/runtime/app/shell/handlers-parity.ts +118 -89
  20. package/runtime/app/shell/handlers-push.ts +5 -3
  21. package/runtime/app/shell/handlers-reviews.ts +5 -6
  22. package/runtime/app/shell/handlers-scanner.ts +36 -38
  23. package/runtime/app/shell/handlers-speech.ts +43 -49
  24. package/runtime/app/shell/handlers-system.ts +1 -1
  25. package/runtime/app/shell/handlers.ts +4 -3
  26. package/runtime/app/shell/ios-frameworks.d.ts +9 -0
  27. package/runtime/app/shell/ios-ui-delegate.ts +112 -108
  28. package/runtime/app/shell/native-log.ts +1 -3
  29. package/runtime/app/shell/status-bar.ts +7 -4
  30. package/runtime/app/shell/toast.ts +1 -1
  31. package/runtime/references.d.ts +6 -0
  32. package/runtime/tsconfig.json +1 -0
  33. package/src/cli.ts +43 -18
  34. package/src/derive.ts +8 -0
@@ -5,6 +5,94 @@ import { uiImageToDataUrl } from './ios-image';
5
5
  const err = (code: string, message: string) => Object.assign(new Error(message), { code });
6
6
  const iosOnly = () => err('UNSUPPORTED', 'iOS only for now');
7
7
 
8
+ /** Read a CNContact into the kit-contract shape (name + phone/email string arrays). */
9
+ function readContact(contact: CNContact): { name: string; phones: string[]; emails: string[] } {
10
+ // CNLabeledValue<T>.value is typed `any` in the SDK, so the per-value map stays loosely typed.
11
+ const collect = (labeled: NSArray<CNLabeledValue<any>>, map: (v: any) => string): string[] => {
12
+ const out: string[] = [];
13
+ for (let i = 0; i < labeled.count; i++) out.push(map(labeled.objectAtIndex(i).value));
14
+ return out;
15
+ };
16
+ return {
17
+ name: `${contact.givenName ?? ''} ${contact.familyName ?? ''}`.trim(),
18
+ phones: collect(contact.phoneNumbers, (v) => String(v.stringValue)),
19
+ emails: collect(contact.emailAddresses, (v) => String(v)),
20
+ };
21
+ }
22
+
23
+ // CLLocationManager delegate for the persistent geo.watch stream. Callbacks are wired via instance
24
+ // fields so the closure-captured behavior of the old (NSObject).extend(...) is preserved.
25
+ @NativeClass()
26
+ class GeoWatchDelegate extends NSObject implements CLLocationManagerDelegate {
27
+ static ObjCProtocols = [CLLocationManagerDelegate];
28
+ static new(): GeoWatchDelegate {
29
+ return <GeoWatchDelegate>super.new();
30
+ }
31
+ locationManagerDidUpdateLocations(_m: CLLocationManager, locations: NSArray<CLLocation>): void {
32
+ const loc = locations.lastObject;
33
+ if (!loc) return;
34
+ bridge.emit('geo.position', {
35
+ lat: loc.coordinate.latitude,
36
+ lng: loc.coordinate.longitude,
37
+ accuracy: loc.horizontalAccuracy,
38
+ });
39
+ }
40
+ locationManagerDidFailWithError(_m: CLLocationManager, error: NSError): void {
41
+ console.warn('AppWrap: geo.watch error', error.localizedDescription);
42
+ }
43
+ locationManagerDidChangeAuthorization(m: CLLocationManager): void {
44
+ const st = m.authorizationStatus;
45
+ if (st === CLAuthorizationStatus.kCLAuthorizationStatusAuthorizedWhenInUse ||
46
+ st === CLAuthorizationStatus.kCLAuthorizationStatusAuthorizedAlways) {
47
+ m.startUpdatingLocation();
48
+ }
49
+ }
50
+ }
51
+
52
+ // CNContactPickerViewController delegate. The picker resolution callbacks are set as instance fields
53
+ // after construction (replacing the old closure-captured `resolve`).
54
+ @NativeClass()
55
+ class ContactPickerDelegate extends NSObject implements CNContactPickerDelegate {
56
+ static ObjCProtocols = [CNContactPickerDelegate];
57
+ static new(): ContactPickerDelegate {
58
+ return <ContactPickerDelegate>super.new();
59
+ }
60
+ onSelect?: (contact: CNContact) => void;
61
+ onCancel?: (picker: CNContactPickerViewController) => void;
62
+ contactPickerDidSelectContact(_picker: CNContactPickerViewController, contact: CNContact): void {
63
+ this.onSelect?.(contact);
64
+ }
65
+ contactPickerDidCancel(picker: CNContactPickerViewController): void {
66
+ this.onCancel?.(picker);
67
+ }
68
+ }
69
+
70
+ // UIImagePickerController delegate (camera.capture). Result/options are set as instance fields after
71
+ // construction, replacing the old closure-captured `resolve`/`dataUrl`/`maxSize`.
72
+ @NativeClass()
73
+ class CameraCaptureDelegate extends NSObject implements UIImagePickerControllerDelegate, UINavigationControllerDelegate {
74
+ static ObjCProtocols = [UIImagePickerControllerDelegate, UINavigationControllerDelegate];
75
+ static new(): CameraCaptureDelegate {
76
+ return <CameraCaptureDelegate>super.new();
77
+ }
78
+ wantDataUrl = false;
79
+ maxSize = 1024;
80
+ onResult?: (out: { picked: boolean; width?: number; height?: number; dataUrl?: string }) => void;
81
+ imagePickerControllerDidFinishPickingMediaWithInfo(picker: UIImagePickerController, info: NSDictionary<string, any>): void {
82
+ picker.dismissViewControllerAnimatedCompletion(true, null);
83
+ const img = info.objectForKey(UIImagePickerControllerOriginalImage) as UIImage | null;
84
+ if (!img) return this.onResult?.({ picked: true });
85
+ const out: { picked: boolean; width?: number; height?: number; dataUrl?: string } =
86
+ { picked: true, width: img.size.width, height: img.size.height };
87
+ if (this.wantDataUrl) out.dataUrl = uiImageToDataUrl(img, this.maxSize);
88
+ this.onResult?.(out);
89
+ }
90
+ imagePickerControllerDidCancel(picker: UIImagePickerController): void {
91
+ picker.dismissViewControllerAnimatedCompletion(true, null);
92
+ this.onResult?.({ picked: false });
93
+ }
94
+ }
95
+
8
96
  /**
9
97
  * Extended-parity handlers: dialogs, storage.clear, geo watch, theme color,
10
98
  * motion, contacts, calendar, camera. iOS-first like handlers-extended.
@@ -50,39 +138,15 @@ export function registerParityHandlers(): void {
50
138
  });
51
139
 
52
140
  // ── geo.watch (persistent CLLocationManager) ───────────────────────
53
- let geoManager: any = null;
54
- let geoDelegate: any = null;
141
+ let geoManager: CLLocationManager | null = null;
142
+ let geoDelegate: GeoWatchDelegate | null = null;
55
143
 
56
144
  bridge.register('geo.watch.start', () => {
57
145
  if (!isIOS) throw iosOnly();
58
146
  if (geoManager) return; // already streaming
59
- const DelegateClass = (NSObject as any).extend(
60
- {
61
- locationManagerDidUpdateLocations(_m: CLLocationManager, locations: NSArray<CLLocation>) {
62
- const loc = locations.lastObject;
63
- if (!loc) return;
64
- bridge.emit('geo.position', {
65
- lat: loc.coordinate.latitude,
66
- lng: loc.coordinate.longitude,
67
- accuracy: loc.horizontalAccuracy,
68
- });
69
- },
70
- locationManagerDidFailWithError(_m: CLLocationManager, error: NSError) {
71
- console.warn('AppWrap: geo.watch error', error.localizedDescription);
72
- },
73
- locationManagerDidChangeAuthorization(m: CLLocationManager) {
74
- const st = m.authorizationStatus;
75
- if (st === CLAuthorizationStatus.kCLAuthorizationStatusAuthorizedWhenInUse ||
76
- st === CLAuthorizationStatus.kCLAuthorizationStatusAuthorizedAlways) {
77
- m.startUpdatingLocation();
78
- }
79
- },
80
- },
81
- { protocols: [CLLocationManagerDelegate] }
82
- );
83
147
  Utils.dispatchToMainThread(() => {
84
148
  geoManager = CLLocationManager.new();
85
- geoDelegate = DelegateClass.new();
149
+ geoDelegate = GeoWatchDelegate.new();
86
150
  geoManager.delegate = geoDelegate;
87
151
  geoManager.desiredAccuracy = kCLLocationAccuracyHundredMeters;
88
152
  const st = geoManager.authorizationStatus;
@@ -105,8 +169,8 @@ export function registerParityHandlers(): void {
105
169
  // block: the repeated JS↔Obj-C block is fragile under NativeScript (the handler
106
170
  // can silently stop firing on-device). startDeviceMotionUpdates() + reading
107
171
  // `mm.deviceMotion` is a plain property read on the main thread — reliable.
108
- let motionManager: any = null;
109
- let motionTimer: any = null;
172
+ let motionManager: CMMotionManager | null = null;
173
+ let motionTimer: ReturnType<typeof setInterval> | null = null;
110
174
 
111
175
  bridge.register('motion.start', () => {
112
176
  if (!isIOS) throw iosOnly();
@@ -143,31 +207,16 @@ export function registerParityHandlers(): void {
143
207
  if (!isIOS) throw iosOnly();
144
208
  return new Promise((resolve) => {
145
209
  Utils.dispatchToMainThread(() => {
146
- const DelegateClass = (NSObject as any).extend(
147
- {
148
- contactPickerDidSelectContact(_picker: any, contact: any) {
149
- const collect = (labeled: NSArray<any>, map: (v: any) => string) => {
150
- const out: string[] = [];
151
- for (let i = 0; i < labeled.count; i++) out.push(map(labeled.objectAtIndex(i).value));
152
- return out;
153
- };
154
- resolve({
155
- picked: true,
156
- name: `${contact.givenName ?? ''} ${contact.familyName ?? ''}`.trim(),
157
- phones: collect(contact.phoneNumbers, (v) => String(v.stringValue)),
158
- emails: collect(contact.emailAddresses, (v) => String(v)),
159
- });
160
- },
161
- contactPickerDidCancel(picker: any) {
162
- picker.dismissViewControllerAnimatedCompletion(true, null);
163
- resolve({ picked: false });
164
- },
165
- },
166
- { protocols: [CNContactPickerDelegate] }
167
- );
210
+ const delegate = ContactPickerDelegate.new();
211
+ delegate.onSelect = (contact) => {
212
+ resolve({ picked: true, ...readContact(contact) });
213
+ };
214
+ delegate.onCancel = (picker) => {
215
+ picker.dismissViewControllerAnimatedCompletion(true, null);
216
+ resolve({ picked: false });
217
+ };
168
218
  const picker = CNContactPickerViewController.new();
169
- const delegate = DelegateClass.new();
170
- (picker as any)._appwrapDelegate = delegate; // retain
219
+ (picker as any)._appwrapDelegate = delegate; // retain delegate against ARC; not a typed property
171
220
  picker.delegate = delegate;
172
221
  Utils.ios.getRootViewController().presentViewControllerAnimatedCompletion(picker, true, null);
173
222
  });
@@ -179,30 +228,23 @@ export function registerParityHandlers(): void {
179
228
  if (!isIOS) throw iosOnly();
180
229
  const store = CNContactStore.new();
181
230
  return new Promise((resolve, reject) => {
182
- store.requestAccessForEntityTypeCompletionHandler(CNEntityType.Contacts, (granted: boolean, error: NSError | null) => {
231
+ store.requestAccessForEntityTypeCompletionHandler(CNEntityType.Contacts, (granted: boolean, error: NSError) => {
183
232
  if (!granted) {
184
233
  return reject(err('DENIED', error?.localizedDescription ?? 'contacts access denied'));
185
234
  }
186
235
  try {
187
- const collect = (labeled: NSArray<any>, map: (v: any) => string) => {
188
- const out: string[] = [];
189
- for (let i = 0; i < labeled.count; i++) out.push(map(labeled.objectAtIndex(i).value));
190
- return out;
191
- };
192
- const keys = NSArray.arrayWithArray([
236
+ // Key string constants conform to CNKeyDescriptor (NSString) at runtime; type the array as such.
237
+ const keys = NSArray.arrayWithArray<CNKeyDescriptor>([
193
238
  CNContactGivenNameKey,
194
239
  CNContactFamilyNameKey,
195
240
  CNContactPhoneNumbersKey,
196
241
  CNContactEmailAddressesKey,
197
- ] as any);
242
+ ]);
198
243
  const request = CNContactFetchRequest.alloc().initWithKeysToFetch(keys);
199
244
  const contacts: Array<{ name: string; phones: string[]; emails: string[] }> = [];
200
- store.enumerateContactsWithFetchRequestErrorUsingBlock(request, null as any, (contact: any) => {
201
- contacts.push({
202
- name: `${contact.givenName ?? ''} ${contact.familyName ?? ''}`.trim(),
203
- phones: collect(contact.phoneNumbers, (v) => String(v.stringValue)),
204
- emails: collect(contact.emailAddresses, (v) => String(v)),
205
- });
245
+ // null error out-param (ObjC nil) — SDK types it as a non-nullable interop reference.
246
+ store.enumerateContactsWithFetchRequestErrorUsingBlock(request, null as any, (contact: CNContact) => {
247
+ contacts.push(readContact(contact));
206
248
  });
207
249
  resolve({ contacts });
208
250
  } catch (e: any) {
@@ -228,8 +270,9 @@ export function registerParityHandlers(): void {
228
270
  const startDate = start
229
271
  ? NSDate.dateWithTimeIntervalSince1970(Date.parse(start) / 1000)
230
272
  : NSDate.dateWithTimeIntervalSinceNow(3600);
231
- event.startDate = startDate;
232
- event.endDate = startDate.dateByAddingTimeInterval((durationMin ?? 60) * 60);
273
+ // EKEvent.start/endDate are typed `Date`; NS marshals the NSDate we build here.
274
+ event.startDate = startDate as unknown as Date;
275
+ event.endDate = startDate.dateByAddingTimeInterval((durationMin ?? 60) * 60) as unknown as Date;
233
276
  if (notes) event.notes = String(notes);
234
277
  event.calendar = store.defaultCalendarForNewEvents;
235
278
  store.saveEventSpanError(event, EKSpan.ThisEvent, null as any);
@@ -249,34 +292,20 @@ export function registerParityHandlers(): void {
249
292
  });
250
293
 
251
294
  // ── camera capture (UIImagePickerController) ───────────────────────
252
- bridge.register('camera.capture', ({ dataUrl, maxSize }: any = {}) => {
295
+ bridge.register('camera.capture', ({ dataUrl, maxSize }: { dataUrl?: boolean; maxSize?: number } = {}) => {
253
296
  if (!isIOS) throw iosOnly();
254
297
  if (!UIImagePickerController.isSourceTypeAvailable(UIImagePickerControllerSourceType.Camera)) {
255
298
  throw err('UNSUPPORTED', 'No camera on this device (simulator?)');
256
299
  }
257
300
  return new Promise((resolve) => {
258
301
  Utils.dispatchToMainThread(() => {
259
- const DelegateClass = (NSObject as any).extend(
260
- {
261
- imagePickerControllerDidFinishPickingMediaWithInfo(picker: any, info: NSDictionary<string, any>) {
262
- picker.dismissViewControllerAnimatedCompletion(true, null);
263
- const img = info.objectForKey(UIImagePickerControllerOriginalImage) as UIImage | null;
264
- if (!img) return resolve({ picked: true });
265
- const out: any = { picked: true, width: img.size.width, height: img.size.height };
266
- if (dataUrl) out.dataUrl = uiImageToDataUrl(img, maxSize ?? 1024);
267
- resolve(out);
268
- },
269
- imagePickerControllerDidCancel(picker: any) {
270
- picker.dismissViewControllerAnimatedCompletion(true, null);
271
- resolve({ picked: false });
272
- },
273
- },
274
- { protocols: [UIImagePickerControllerDelegate, UINavigationControllerDelegate] }
275
- );
276
302
  const picker = UIImagePickerController.new();
277
303
  picker.sourceType = UIImagePickerControllerSourceType.Camera;
278
- const delegate = DelegateClass.new();
279
- (picker as any)._appwrapDelegate = delegate; // retain
304
+ const delegate = CameraCaptureDelegate.new();
305
+ delegate.wantDataUrl = !!dataUrl;
306
+ delegate.maxSize = maxSize ?? 1024;
307
+ delegate.onResult = resolve;
308
+ (picker as any)._appwrapDelegate = delegate; // interop: retain delegate past the present call (no typed slot)
280
309
  picker.delegate = delegate;
281
310
  Utils.ios.getRootViewController().presentViewControllerAnimatedCompletion(picker, true, null);
282
311
  });
@@ -62,7 +62,7 @@ export function onApnsError(message: string): void {
62
62
  }
63
63
 
64
64
  /** iOS AppDelegate / UN delegate → a remote notification arrived (tapped = user opened it). */
65
- export function onRemoteMessage(userInfo: any, tapped: boolean): void {
65
+ export function onRemoteMessage(userInfo: any, tapped: boolean): void { // any: APNs userInfo is NSDictionary or auto-marshalled JS object (dual-path)
66
66
  bridge.emit(tapped ? 'push.tap' : 'push.message', parseApnsPayload(userInfo));
67
67
  }
68
68
 
@@ -74,6 +74,7 @@ export function onFcmMessage(data: Record<string, string>, title?: string, body?
74
74
  }
75
75
 
76
76
  /** APNs userInfo (NSDictionary or auto-marshalled object) → { data, title?, body? }. */
77
+ // any: dual-path payload (NSDictionary vs marshalled JS object) probed dynamically below.
77
78
  function parseApnsPayload(userInfo: any): { data: Record<string, unknown>; title?: string; body?: string } {
78
79
  const data: Record<string, unknown> = {};
79
80
  let title: string | undefined;
@@ -181,7 +182,7 @@ function unsupported(): Error {
181
182
  }
182
183
 
183
184
  // ── Android (Firebase Messaging) — guarded; classes exist only when FCM is wired ──
184
- function fcm(): any {
185
+ function fcm(): any { // no NS types: third-party firebase (com.google.firebase.messaging)
185
186
  const FM = (global as any).com?.google?.firebase?.messaging?.FirebaseMessaging;
186
187
  if (!FM) throw Object.assign(new Error('FCM not configured — wire google-services.json + the messaging plugin'), { code: 'UNSUPPORTED' });
187
188
  return FM.getInstance();
@@ -198,6 +199,7 @@ function androidRequestPermission(): Promise<string> {
198
199
  const granted = ctx.checkSelfPermission('android.permission.POST_NOTIFICATIONS') === pm.PERMISSION_GRANTED;
199
200
  if (granted) return resolve('granted');
200
201
  const activity = Application.android.foregroundActivity || Application.android.startActivity;
202
+ // any: NS activity-result event payload (grantResults/requestCode) is untyped in core's event bus.
201
203
  Application.android.on(Application.android.activityRequestPermissionsEvent as any, (args: any) => {
202
204
  if (args.requestCode !== 7613) return;
203
205
  const ok = args.grantResults?.length && args.grantResults[0] === pm.PERMISSION_GRANTED;
@@ -229,7 +231,7 @@ function androidGetToken(): Promise<PushToken> {
229
231
  const OnCompleteListener = (global as any).com.google.android.gms.tasks.OnCompleteListener;
230
232
  const task = fcm().getToken();
231
233
  task.addOnCompleteListener(new OnCompleteListener({
232
- onComplete(t: any) {
234
+ onComplete(t: any) { // no NS types: play-services gms Task<String> (FCM token)
233
235
  if (t.isSuccessful()) {
234
236
  const tok = String(t.getResult());
235
237
  registerTokenWithBackend('android', tok); // parity with iOS onApnsToken
@@ -1,9 +1,8 @@
1
1
  import { Application, Utils, isAndroid, isIOS } from '@nativescript/core';
2
2
  import { bridge } from './bridge';
3
3
 
4
- // SKStoreReviewController comes from the StoreKit typings referenced in references.d.ts. `com` is the
5
- // Android namespace.
6
- declare const com: any;
4
+ // SKStoreReviewController comes from the StoreKit typings referenced in references.d.ts.
5
+ declare const com: any; // no NS types: third-party play-core review (com.google.android.play.core.review)
7
6
 
8
7
  /**
9
8
  * In-app store review prompt — opt-in, strippable (its own handler file + group). Lives apart from
@@ -28,8 +27,8 @@ function registerIos(): void {
28
27
  bridge.register('reviews.requestReview', () => {
29
28
  Utils.dispatchToMainThread(() => {
30
29
  const scene = UIApplication.sharedApplication.keyWindow?.windowScene;
31
- if (scene && SKStoreReviewController.requestReviewInWindowScene) {
32
- SKStoreReviewController.requestReviewInWindowScene(scene);
30
+ if (scene && SKStoreReviewController.requestReviewInScene) {
31
+ SKStoreReviewController.requestReviewInScene(scene);
33
32
  } else {
34
33
  SKStoreReviewController.requestReview();
35
34
  }
@@ -50,7 +49,7 @@ function registerAndroid(): void {
50
49
  // Both tasks COMPLETE even when the OS shows nothing (no Play track / rate-limited), so we
51
50
  // resolve on completion regardless — never block, never fabricate a "shown" signal.
52
51
  request.addOnCompleteListener(new com.google.android.gms.tasks.OnCompleteListener({
53
- onComplete(infoTask: any) {
52
+ onComplete(infoTask: any) { // no NS types: play-services gms Task<ReviewInfo>
54
53
  if (!infoTask.isSuccessful()) { resolve(); return; }
55
54
  try {
56
55
  const flow = manager.launchReviewFlow(activity, infoTask.getResult());
@@ -2,30 +2,36 @@ import { Utils, isAndroid, isIOS } from '@nativescript/core';
2
2
  import { bridge } from './bridge';
3
3
  import { requestPermissions, startActivityForResult } from './android-helpers';
4
4
 
5
- // AVFoundation / UIKit globals (NS auto-links the frameworks via metadata when referenced).
6
- declare const AVCaptureSession: any;
7
- declare const AVCaptureDevice: any;
8
- declare const AVCaptureDeviceInput: any;
9
- declare const AVCaptureMetadataOutput: any;
10
- declare const AVCaptureVideoPreviewLayer: any;
11
- declare const AVMetadataObjectTypeQRCode: any;
12
- declare const AVMetadataObjectTypeEAN13Code: any;
13
- declare const AVMetadataObjectTypeCode128Code: any;
14
- declare const AVLayerVideoGravityResizeAspectFill: any;
15
- declare const AVCaptureDevicePositionFront: any;
16
- declare const AVCaptureDevicePositionBack: any;
17
- declare const AVCaptureDeviceDiscoverySession: any;
18
- declare const AVMediaTypeVideo: any;
19
- declare const AVCaptureDeviceTypeBuiltInWideAngleCamera: any;
20
- declare const AVCaptureMetadataOutputObjectsDelegate: any;
21
5
  // `dispatch_get_main_queue()` is a C macro, NOT a bridged symbol — NativeScript can't see it
22
6
  // ("is not defined" at runtime). `dispatch_queue_create` IS a real bridged libdispatch function;
23
7
  // a dedicated serial queue is the correct delegate queue for AVCaptureMetadataOutput anyway.
24
8
  declare function dispatch_queue_create(label: string, attr: any): any;
25
- declare const android: any, com: any;
9
+ declare const com: any; // no NS types: third-party ZXing (com.google.zxing.integration.android.*)
26
10
 
27
11
  const err = (code: string, message: string) => Object.assign(new Error(message), { code });
28
12
 
13
+ // iOS metadata-output delegate. NativeScript uses the declared protocol to map the camelCase JS
14
+ // method to the colonated selector `captureOutput:didOutputMetadataObjects:fromConnection:`; with an
15
+ // empty protocols list it registers under the wrong selector and AVFoundation never calls it — the
16
+ // scanner opens but never decodes. The first decoded object is forwarded via the `onResult` instance
17
+ // field (assigned after `new()`), replacing the original closure capture.
18
+ @NativeClass()
19
+ class MetadataDelegate extends NSObject implements AVCaptureMetadataOutputObjectsDelegate {
20
+ static ObjCProtocols = [AVCaptureMetadataOutputObjectsDelegate];
21
+ onResult?: (obj: AVMetadataObject) => void;
22
+ static new(): MetadataDelegate {
23
+ return <MetadataDelegate>super.new();
24
+ }
25
+ captureOutputDidOutputMetadataObjectsFromConnection(
26
+ _output: AVCaptureOutput,
27
+ metadataObjects: NSArray<AVMetadataObject> | AVMetadataObject[],
28
+ _connection: AVCaptureConnection
29
+ ): void {
30
+ const obj = (metadataObjects as NSArray<AVMetadataObject>)?.firstObject;
31
+ if (obj) this.onResult?.(obj);
32
+ }
33
+ }
34
+
29
35
  /**
30
36
  * Camera barcode / QR decoding. iOS: AVCaptureMetadataOutput in a full-screen capture VC presented
31
37
  * over the WebView; first machine-readable code → stop session, dismiss, resolve. Android: ZXing's
@@ -41,12 +47,12 @@ export function registerScannerHandlers(): void {
41
47
  }
42
48
 
43
49
  /** Map our small enum → the platform metadata-object types. Empty/`all` = the full supported set. */
44
- function iosFormatTypes(formats: any): any[] {
50
+ function iosFormatTypes(formats: any): string[] {
45
51
  const all = [AVMetadataObjectTypeQRCode, AVMetadataObjectTypeEAN13Code, AVMetadataObjectTypeCode128Code];
46
52
  const want = Array.isArray(formats) ? formats : formats ? [formats] : [];
47
53
  const sel = want.filter((f: string) => f && f !== 'all');
48
54
  if (!sel.length) return all;
49
- const map: Record<string, any> = {
55
+ const map: Record<string, string> = {
50
56
  qr: AVMetadataObjectTypeQRCode,
51
57
  ean13: AVMetadataObjectTypeEAN13Code,
52
58
  code128: AVMetadataObjectTypeCode128Code,
@@ -54,7 +60,7 @@ function iosFormatTypes(formats: any): any[] {
54
60
  return sel.map((f: string) => map[f]).filter(Boolean);
55
61
  }
56
62
 
57
- function iosFormatName(type: any): string {
63
+ function iosFormatName(type: string): string {
58
64
  if (type === AVMetadataObjectTypeQRCode) return 'qr';
59
65
  if (type === AVMetadataObjectTypeEAN13Code) return 'ean13';
60
66
  if (type === AVMetadataObjectTypeCode128Code) return 'code128';
@@ -98,7 +104,7 @@ function registerIos(): void {
98
104
  Utils.dispatchToMainThread(() => {
99
105
  try {
100
106
  const wantFront = params?.camera === 'front';
101
- const position = wantFront ? AVCaptureDevicePositionFront : AVCaptureDevicePositionBack;
107
+ const position = wantFront ? AVCaptureDevicePosition.Front : AVCaptureDevicePosition.Back;
102
108
  const discovery = AVCaptureDeviceDiscoverySession.discoverySessionWithDeviceTypesMediaTypePosition(
103
109
  [AVCaptureDeviceTypeBuiltInWideAngleCamera], AVMediaTypeVideo, position
104
110
  );
@@ -118,25 +124,17 @@ function registerIos(): void {
118
124
 
119
125
  // Delegate fires on a dedicated serial queue (see dispatch_queue_create below); settle()
120
126
  // hops back to the main thread for the WKWebView bridge reply.
121
- const DelegateClass = (NSObject as any).extend(
122
- {
123
- captureOutputDidOutputMetadataObjectsFromConnection(_out: any, objects: any, _conn: any) {
124
- if (!pending) return;
125
- const obj = objects?.firstObject;
126
- const value = obj?.stringValue;
127
- if (value == null) return;
128
- settle({ value: String(value), format: iosFormatName(obj.type) });
129
- },
130
- },
131
- // The protocol MUST be declared: NativeScript uses it to map the camelCase JS method to the
132
- // colonated selector `captureOutput:didOutputMetadataObjects:fromConnection:`. With an empty
133
- // protocols list the method registers under the wrong selector and AVFoundation never calls
134
- // it — the scanner opens but never decodes.
135
- { protocols: [AVCaptureMetadataOutputObjectsDelegate] }
136
- );
137
- activeDelegate = DelegateClass.new();
127
+ const delegate = MetadataDelegate.new();
128
+ delegate.onResult = (obj) => {
129
+ if (!pending) return;
130
+ const value = (obj as AVMetadataMachineReadableCodeObject).stringValue;
131
+ if (value == null) return;
132
+ settle({ value: String(value), format: iosFormatName(obj.type) });
133
+ };
134
+ activeDelegate = delegate;
138
135
  output.setMetadataObjectsDelegateQueue(activeDelegate, dispatch_queue_create('cc.livx.scanner.metadata', null));
139
136
  // metadataObjectTypes must be set AFTER addOutput (the available set is session-derived).
137
+ // interop: JS string[] is accepted at runtime where NSArray<string> is declared.
140
138
  output.metadataObjectTypes = iosFormatTypes(params?.formats) as any;
141
139
 
142
140
  const vc = UIViewController.new();
@@ -2,23 +2,25 @@ import { Utils, isAndroid, isIOS } from '@nativescript/core';
2
2
  import { bridge } from './bridge';
3
3
  import { requestPermissions } from './android-helpers';
4
4
 
5
- // AVFoundation / Speech globals (NS auto-links the frameworks via metadata when referenced).
6
- declare const AVSpeechSynthesizer: any;
7
- declare const AVSpeechUtterance: any;
8
- declare const AVSpeechSynthesisVoice: any;
9
- declare const AVAudioEngine: any;
10
- declare const AVAudioSession: any;
11
- declare const AVAudioSessionCategoryPlayAndRecord: any;
12
- declare const AVAudioSessionModeMeasurement: any;
13
- declare const SFSpeechRecognizer: any;
14
- declare const SFSpeechAudioBufferRecognitionRequest: any;
15
- declare const SFSpeechRecognizerAuthorizationStatusAuthorized: any;
16
- declare const NSLocale: any;
17
- declare const android: any;
18
- declare const java: any;
19
-
20
5
  const err = (code: string, message: string) => Object.assign(new Error(message), { code });
21
6
 
7
+ // iOS speech-synthesizer delegate — both finish & cancel settle the pending speak (the JS
8
+ // `speakPending` is captured via the `onSettle` instance field, assigned after `new()`).
9
+ @NativeClass()
10
+ class SpeakDelegate extends NSObject implements AVSpeechSynthesizerDelegate {
11
+ static ObjCProtocols = [AVSpeechSynthesizerDelegate];
12
+ onSettle?: () => void;
13
+ static new(): SpeakDelegate {
14
+ return <SpeakDelegate>super.new();
15
+ }
16
+ speechSynthesizerDidFinishSpeechUtterance(_s: AVSpeechSynthesizer, _u: AVSpeechUtterance): void {
17
+ this.onSettle?.();
18
+ }
19
+ speechSynthesizerDidCancelSpeechUtterance(_s: AVSpeechSynthesizer, _u: AVSpeechUtterance): void {
20
+ this.onSettle?.();
21
+ }
22
+ }
23
+
22
24
  /**
23
25
  * Voice I/O. TTS: iOS AVSpeechSynthesizer / Android TextToSpeech — speak resolves on finish.
24
26
  * STT: iOS SFSpeechRecognizer + AVAudioEngine mic tap / Android SpeechRecognizer — listen resolves
@@ -37,15 +39,15 @@ export function registerSpeechHandlers(): void {
37
39
  function registerIos(): void {
38
40
  // Strong refs while a session is live — ARC would otherwise free the synthesizer/engine/delegate
39
41
  // the moment the JS locals fall out of scope (same hazard as handlers-scanner/oauth).
40
- let synth: any = null;
41
- let speakDelegate: any = null;
42
+ let synth: AVSpeechSynthesizer | null = null;
43
+ let speakDelegate: SpeakDelegate | null = null;
42
44
  let speakPending: { resolve: () => void } | null = null;
43
45
 
44
- let engine: any = null;
45
- let recognizer: any = null;
46
- let request: any = null;
47
- let task: any = null;
48
- let listenPending: { resolve: (v: string) => void; reject: (e: any) => void } | null = null;
46
+ let engine: AVAudioEngine | null = null;
47
+ let recognizer: SFSpeechRecognizer | null = null;
48
+ let request: SFSpeechAudioBufferRecognitionRequest | null = null;
49
+ let task: SFSpeechRecognitionTask | null = null;
50
+ let listenPending: { resolve: (v: string) => void; reject: (e: Error) => void } | null = null;
49
51
  let best = '';
50
52
 
51
53
  const teardownListen = () => {
@@ -71,18 +73,8 @@ function registerIos(): void {
71
73
  try {
72
74
  if (!synth) {
73
75
  synth = AVSpeechSynthesizer.new();
74
- const DelegateClass = (NSObject as any).extend(
75
- {
76
- speechSynthesizerDidFinishSpeechUtterance(_s: any, _u: any) {
77
- const p = speakPending; speakPending = null; p?.resolve();
78
- },
79
- speechSynthesizerDidCancelSpeechUtterance(_s: any, _u: any) {
80
- const p = speakPending; speakPending = null; p?.resolve();
81
- },
82
- },
83
- { protocols: [] }
84
- );
85
- speakDelegate = DelegateClass.new();
76
+ speakDelegate = SpeakDelegate.new();
77
+ speakDelegate.onSettle = () => { const p = speakPending; speakPending = null; p?.resolve(); };
86
78
  synth.delegate = speakDelegate;
87
79
  }
88
80
  // A new speak resolves any prior pending (its utterance is being superseded).
@@ -136,8 +128,8 @@ function registerIos(): void {
136
128
  best = '';
137
129
  listenPending = { resolve, reject };
138
130
 
139
- SFSpeechRecognizer.requestAuthorization((status: any) => {
140
- if (status !== SFSpeechRecognizerAuthorizationStatusAuthorized) {
131
+ SFSpeechRecognizer.requestAuthorization((status: SFSpeechRecognizerAuthorizationStatus) => {
132
+ if (status !== SFSpeechRecognizerAuthorizationStatus.Authorized) {
141
133
  const p = listenPending; listenPending = null;
142
134
  p?.reject(err('DENIED', 'Speech recognition not authorized'));
143
135
  return;
@@ -167,17 +159,18 @@ function registerIos(): void {
167
159
 
168
160
  const session = AVAudioSession.sharedInstance();
169
161
  session.setCategoryModeOptionsError(AVAudioSessionCategoryPlayAndRecord, AVAudioSessionModeMeasurement, 1 /* duckOthers */, null);
170
- session.setActiveWithOptionsError(true, 0, null);
162
+ // 0 = no activation flags; not a named member of the options enum, so cast (preserve value).
163
+ session.setActiveWithOptionsError(true, 0 as AVAudioSessionSetActiveOptions, null);
171
164
 
172
165
  engine = AVAudioEngine.new();
173
166
  request = SFSpeechAudioBufferRecognitionRequest.new();
174
167
  request.shouldReportPartialResults = !!params?.partial;
175
168
 
176
- task = recognizer.recognitionTaskWithRequestResultHandler(request, (result: any, error: any) => {
169
+ task = recognizer.recognitionTaskWithRequestResultHandler(request, (result: SFSpeechRecognitionResult, error: NSError) => {
177
170
  if (result) {
178
171
  best = String(result.bestTranscription?.formattedString ?? best);
179
- if (params?.partial && !result.isFinal) bridge.emit('speech.partial', { transcript: best });
180
- if (result.isFinal && listenPending) settleListen();
172
+ if (params?.partial && !result.final) bridge.emit('speech.partial', { transcript: best });
173
+ if (result.final && listenPending) settleListen();
181
174
  }
182
175
  if (error && listenPending) {
183
176
  const p = listenPending; listenPending = null;
@@ -188,7 +181,7 @@ function registerIos(): void {
188
181
 
189
182
  const input = engine.inputNode;
190
183
  const format = input.outputFormatForBus(0);
191
- input.installTapOnBusBufferSizeFormatBlock(0, 1024, format, (buffer: any, _when: any) => {
184
+ input.installTapOnBusBufferSizeFormatBlock(0, 1024, format, (buffer: AVAudioPCMBuffer, _when: AVAudioTime) => {
192
185
  request?.appendAudioPCMBuffer(buffer);
193
186
  });
194
187
  engine.prepare();
@@ -206,11 +199,11 @@ function registerIos(): void {
206
199
 
207
200
  function registerAndroid(): void {
208
201
  // TextToSpeech + SpeechRecognizer are plain Java → direct interop, no kotlin flag, no gradle dep.
209
- let tts: any = null;
202
+ let tts: android.speech.tts.TextToSpeech | null = null;
210
203
  let ttsReady = false;
211
204
  const ctx = () => Utils.android.getApplicationContext();
212
205
 
213
- const ensureTts = (): Promise<any> =>
206
+ const ensureTts = (): Promise<android.speech.tts.TextToSpeech | null> =>
214
207
  new Promise((resolve) => {
215
208
  if (tts && ttsReady) { resolve(tts); return; }
216
209
  tts = new android.speech.tts.TextToSpeech(
@@ -236,7 +229,8 @@ function registerAndroid(): void {
236
229
  const id = 'u' + Date.now();
237
230
  return new Promise<void>((resolve) => {
238
231
  engine.setOnUtteranceProgressListener(
239
- new android.speech.tts.UtteranceProgressListener({
232
+ // NS interop: subclass the abstract Java class via object-literal ctor (untypeable — cast base).
233
+ new (android.speech.tts.UtteranceProgressListener as any)({
240
234
  onStart() {},
241
235
  onDone() { resolve(); },
242
236
  onError() { resolve(); }, // resolve regardless — speak() promises completion, not success
@@ -264,8 +258,8 @@ function registerAndroid(): void {
264
258
  });
265
259
 
266
260
  // STT — SpeechRecognizer must be created + driven on the main (UI) thread.
267
- let recognizer: any = null;
268
- let listenPending: { resolve: (v: string) => void; reject: (e: any) => void } | null = null;
261
+ let recognizer: android.speech.SpeechRecognizer | null = null;
262
+ let listenPending: { resolve: (v: string) => void; reject: (e: Error) => void } | null = null;
269
263
  let best = '';
270
264
 
271
265
  const settle = () => {
@@ -295,7 +289,7 @@ function registerAndroid(): void {
295
289
  return;
296
290
  }
297
291
  recognizer = android.speech.SpeechRecognizer.createSpeechRecognizer(ctx());
298
- const pickText = (b: any): string => {
292
+ const pickText = (b: android.os.Bundle): string => {
299
293
  const arr = b?.getStringArrayList(android.speech.SpeechRecognizer.RESULTS_RECOGNITION);
300
294
  return arr && arr.size() ? String(arr.get(0)) : '';
301
295
  };
@@ -303,12 +297,12 @@ function registerAndroid(): void {
303
297
  new android.speech.RecognitionListener({
304
298
  onReadyForSpeech() {}, onBeginningOfSpeech() {}, onRmsChanged() {},
305
299
  onBufferReceived() {}, onEndOfSpeech() {}, onEvent() {},
306
- onPartialResults(b: any) {
300
+ onPartialResults(b: android.os.Bundle) {
307
301
  if (!params?.partial) return;
308
302
  const t = pickText(b);
309
303
  if (t) { best = t; bridge.emit('speech.partial', { transcript: t }); }
310
304
  },
311
- onResults(b: any) {
305
+ onResults(b: android.os.Bundle) {
312
306
  const t = pickText(b);
313
307
  if (t) best = t;
314
308
  if (listenPending) settle();