@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
@@ -1,30 +1,22 @@
1
1
  import { Utils, isIOS } from '@nativescript/core';
2
2
  import { bridge } from './bridge';
3
3
 
4
- // AVAudioSession globals (NS resolves these from the AVFoundation metadata at runtime).
5
- declare const AVAudioSession: any;
6
- declare const AVAudioSessionCategoryPlayAndRecord: string;
7
- declare const AVAudioSessionPortBuiltInReceiver: string;
8
- declare const AVAudioSessionRouteChangeNotification: string;
9
- declare const AVAudioSessionPortOverride: any;
10
- declare const NSNotificationCenter: any;
11
-
12
4
  let routeObserverArmed = false;
13
5
 
14
6
  /** List the current output port types (e.g. "Receiver", "Speaker", "BluetoothA2DP"). */
15
- function outputPortTypes(session: any): string[] {
7
+ function outputPortTypes(session: AVAudioSession): string[] {
16
8
  const outputs = session.currentRoute?.outputs;
17
9
  const types: string[] = [];
18
10
  for (let i = 0; i < (outputs?.count ?? 0); i++) types.push(String(outputs.objectAtIndex(i).portType));
19
11
  return types;
20
12
  }
21
13
 
22
- function isOnBuiltInReceiver(session: any): boolean {
14
+ function isOnBuiltInReceiver(session: AVAudioSession): boolean {
23
15
  return outputPortTypes(session).indexOf(AVAudioSessionPortBuiltInReceiver) !== -1;
24
16
  }
25
17
 
26
18
  /** Whether the session can record (PlayAndRecord) — i.e. we're (or about to be) in a call. */
27
- function isRecordCategory(session: any): boolean {
19
+ function isRecordCategory(session: AVAudioSession): boolean {
28
20
  return String(session.category) === String(AVAudioSessionCategoryPlayAndRecord);
29
21
  }
30
22
 
@@ -33,7 +25,7 @@ function forceSpeakerIfReceiverDuringCall(): void {
33
25
  const session = AVAudioSession.sharedInstance();
34
26
  if (!isRecordCategory(session) || !isOnBuiltInReceiver(session)) return; // not a call, or already speaker/headset/BT
35
27
  try {
36
- session.overrideOutputAudioPortError(AVAudioSessionPortOverride.Speaker, null);
28
+ session.overrideOutputAudioPortError(AVAudioSessionPortOverride.Speaker);
37
29
  } catch {
38
30
  /* benign — route may have changed again under us */
39
31
  }
@@ -60,6 +52,7 @@ function observeAudioRouteForSpeaker(): void {
60
52
  */
61
53
  export function registerMediaHandlers(): void {
62
54
  // DIAGNOSTIC: surface the WKWebView UIDelegate status to the page (on-screen log).
55
+ // no NS types: __appwrapWebviewDiag is an ad-hoc diagnostic stashed on global by the WKWebView UIDelegate.
63
56
  bridge.register('debug.webviewInfo', () => ({ diag: (global as any).__appwrapWebviewDiag ?? 'n/a (android/web)' }));
64
57
 
65
58
  // Auto loudspeaker for in-WebView WebRTC calls. When getUserMedia is live, WebKit drives the
@@ -120,7 +113,8 @@ export function registerMediaHandlers(): void {
120
113
  } else {
121
114
  category = AVAudioSessionCategoryPlayback; // ignores the silent switch
122
115
  }
123
- session.setCategoryModeOptionsError(category, avMode, options as any, null as any);
124
- session.setActiveError(true, null as any);
116
+ // options is an OR-combined flag mask; the bitwise result widens to number, so cast back to the enum.
117
+ session.setCategoryModeOptionsError(category, avMode, options as AVAudioSessionCategoryOptions);
118
+ session.setActiveError(true);
125
119
  });
126
120
  }
@@ -2,17 +2,28 @@ import { Application, Utils, isAndroid, isIOS } from '@nativescript/core';
2
2
  import { bridge } from './bridge';
3
3
  import { setDeepLinkInterceptor } from './events';
4
4
 
5
- // AuthenticationServices globals (NS auto-links the framework via metadata when referenced).
6
- declare const ASWebAuthenticationSession: any;
7
- declare const ASWebAuthenticationPresentationContextProviding: any;
8
- declare const android: any, androidx: any;
5
+ declare const androidx: any; // no NS types: androidx
9
6
 
10
7
  const err = (code: string, message: string) => Object.assign(new Error(message), { code });
11
8
 
12
9
  // Keep strong refs while the sheet is up — ARC frees the session + its context provider the moment
13
10
  // the JS locals fall out of scope, which silently cancels the flow mid-handshake.
14
- let activeSession: any = null;
15
- let activeProvider: any = null;
11
+ let activeSession: ASWebAuthenticationSession | null = null;
12
+ let activeProvider: ASWebAuthenticationPresentationContextProviding | null = null;
13
+
14
+ @NativeClass()
15
+ class OAuthPresentationContextProvider extends NSObject implements ASWebAuthenticationPresentationContextProviding {
16
+ static ObjCProtocols = [ASWebAuthenticationPresentationContextProviding];
17
+ static new(): OAuthPresentationContextProvider {
18
+ return <OAuthPresentationContextProvider>super.new();
19
+ }
20
+ presentationAnchorForWebAuthenticationSession(_session: ASWebAuthenticationSession): UIWindow {
21
+ return (
22
+ Utils.ios.getRootViewController()?.view?.window ??
23
+ UIApplication.sharedApplication.keyWindow
24
+ );
25
+ }
26
+ }
16
27
 
17
28
  /**
18
29
  * System-browser OAuth (iOS ASWebAuthenticationSession). Runs the provider's auth page in the
@@ -37,7 +48,7 @@ export function registerOAuthHandlers(): void {
37
48
  const session = ASWebAuthenticationSession.alloc().initWithURLCallbackURLSchemeCompletionHandler(
38
49
  NSURL.URLWithString(target),
39
50
  scheme,
40
- (callbackURL: any, error: any) => {
51
+ (callbackURL: NSURL, error: NSError) => {
41
52
  activeSession = null;
42
53
  activeProvider = null;
43
54
  if (error) {
@@ -50,18 +61,7 @@ export function registerOAuthHandlers(): void {
50
61
  }
51
62
  );
52
63
 
53
- const ProviderClass = (NSObject as any).extend(
54
- {
55
- presentationAnchorForWebAuthenticationSession(_session: any): any {
56
- return (
57
- Utils.ios.getRootViewController()?.view?.window ??
58
- UIApplication.sharedApplication.keyWindow
59
- );
60
- },
61
- },
62
- { protocols: [ASWebAuthenticationPresentationContextProviding] }
63
- );
64
- activeProvider = ProviderClass.new();
64
+ activeProvider = OAuthPresentationContextProvider.new();
65
65
  session.presentationContextProvider = activeProvider;
66
66
  session.prefersEphemeralWebBrowserSession = !!ephemeral;
67
67
  activeSession = session;
@@ -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;
@@ -166,18 +167,6 @@ export function registerPushHandlers(): void {
166
167
  if (isAndroid) return androidDeleteToken();
167
168
  });
168
169
 
169
- // Dev/demo: ask the backend to push THIS device on demand. Goes native (NS Http) → no app:// CORS
170
- // wall. Requires push.registrationUrl + a registered token; the backend honors `test:true`.
171
- bridge.register('push.sendTest', () => {
172
- const url = SHELL_CONFIG.pushRegistrationUrl;
173
- if (!url) return Promise.reject(Object.assign(new Error('push.registrationUrl not configured'), { code: 'UNSUPPORTED' }));
174
- if (!cachedToken) return Promise.reject(Object.assign(new Error('no device token yet — register first'), { code: 'NOT_READY' }));
175
- return Http.request({
176
- url, method: 'POST', headers: { 'Content-Type': 'application/json' },
177
- content: JSON.stringify({ token: cachedToken, platform: isIOS ? 'ios' : 'android', test: true }),
178
- }).then((res) => ({ status: res.statusCode }));
179
- });
180
-
181
170
  // Debug: auto-acquire + log the FCM token at boot so it's readable HEADLESSLY (adb logcat / appwrap
182
171
  // logs) without a UI tap — the dev loop needs the token to send a test push. Android only (iOS push
183
172
  // is off on personal teams); no-op when push isn't configured for this platform.
@@ -193,7 +182,7 @@ function unsupported(): Error {
193
182
  }
194
183
 
195
184
  // ── Android (Firebase Messaging) — guarded; classes exist only when FCM is wired ──
196
- function fcm(): any {
185
+ function fcm(): any { // no NS types: third-party firebase (com.google.firebase.messaging)
197
186
  const FM = (global as any).com?.google?.firebase?.messaging?.FirebaseMessaging;
198
187
  if (!FM) throw Object.assign(new Error('FCM not configured — wire google-services.json + the messaging plugin'), { code: 'UNSUPPORTED' });
199
188
  return FM.getInstance();
@@ -210,6 +199,7 @@ function androidRequestPermission(): Promise<string> {
210
199
  const granted = ctx.checkSelfPermission('android.permission.POST_NOTIFICATIONS') === pm.PERMISSION_GRANTED;
211
200
  if (granted) return resolve('granted');
212
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.
213
203
  Application.android.on(Application.android.activityRequestPermissionsEvent as any, (args: any) => {
214
204
  if (args.requestCode !== 7613) return;
215
205
  const ok = args.grantResults?.length && args.grantResults[0] === pm.PERMISSION_GRANTED;
@@ -241,7 +231,7 @@ function androidGetToken(): Promise<PushToken> {
241
231
  const OnCompleteListener = (global as any).com.google.android.gms.tasks.OnCompleteListener;
242
232
  const task = fcm().getToken();
243
233
  task.addOnCompleteListener(new OnCompleteListener({
244
- onComplete(t: any) {
234
+ onComplete(t: any) { // no NS types: play-services gms Task<String> (FCM token)
245
235
  if (t.isSuccessful()) {
246
236
  const tok = String(t.getResult());
247
237
  registerTokenWithBackend('android', tok); // parity with iOS onApnsToken
@@ -1,8 +1,8 @@
1
1
  import { Application, Utils, isAndroid, isIOS } from '@nativescript/core';
2
2
  import { bridge } from './bridge';
3
3
 
4
- // SKStoreReviewController is declared globally in ios-frameworks.d.ts. `com` is the Android namespace.
5
- 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)
6
6
 
7
7
  /**
8
8
  * In-app store review prompt — opt-in, strippable (its own handler file + group). Lives apart from
@@ -27,8 +27,8 @@ function registerIos(): void {
27
27
  bridge.register('reviews.requestReview', () => {
28
28
  Utils.dispatchToMainThread(() => {
29
29
  const scene = UIApplication.sharedApplication.keyWindow?.windowScene;
30
- if (scene && SKStoreReviewController.requestReviewInWindowScene) {
31
- SKStoreReviewController.requestReviewInWindowScene(scene);
30
+ if (scene && SKStoreReviewController.requestReviewInScene) {
31
+ SKStoreReviewController.requestReviewInScene(scene);
32
32
  } else {
33
33
  SKStoreReviewController.requestReview();
34
34
  }
@@ -49,7 +49,7 @@ function registerAndroid(): void {
49
49
  // Both tasks COMPLETE even when the OS shows nothing (no Play track / rate-limited), so we
50
50
  // resolve on completion regardless — never block, never fabricate a "shown" signal.
51
51
  request.addOnCompleteListener(new com.google.android.gms.tasks.OnCompleteListener({
52
- onComplete(infoTask: any) {
52
+ onComplete(infoTask: any) { // no NS types: play-services gms Task<ReviewInfo>
53
53
  if (!infoTask.isSuccessful()) { resolve(); return; }
54
54
  try {
55
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();