@livx.cc/appwrap 0.28.0 → 0.28.2

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/app.ts +55 -54
  3. package/runtime/app/shell/android-helpers.ts +6 -6
  4. package/runtime/app/shell/background-worker.android.ts +47 -0
  5. package/runtime/app/shell/background-worker.ios.ts +5 -0
  6. package/runtime/app/shell/background-worker.ts +7 -0
  7. package/runtime/app/shell/banner.ts +9 -4
  8. package/runtime/app/shell/bridge.ts +2 -0
  9. package/runtime/app/shell/custom-webview.android.ts +50 -23
  10. package/runtime/app/shell/custom-webview.ios.ts +90 -78
  11. package/runtime/app/shell/devmenu.ts +2 -2
  12. package/runtime/app/shell/env.ts +1 -3
  13. package/runtime/app/shell/events.ts +5 -4
  14. package/runtime/app/shell/handlers-android.ts +16 -12
  15. package/runtime/app/shell/handlers-background.ts +57 -78
  16. package/runtime/app/shell/handlers-billing.ts +90 -82
  17. package/runtime/app/shell/handlers-extended.ts +141 -110
  18. package/runtime/app/shell/handlers-fs.ts +53 -41
  19. package/runtime/app/shell/handlers-health.ts +27 -19
  20. package/runtime/app/shell/handlers-media.ts +8 -14
  21. package/runtime/app/shell/handlers-oauth.ts +19 -19
  22. package/runtime/app/shell/handlers-parity.ts +118 -89
  23. package/runtime/app/shell/handlers-push.ts +5 -3
  24. package/runtime/app/shell/handlers-reviews.ts +5 -6
  25. package/runtime/app/shell/handlers-scanner.ts +36 -38
  26. package/runtime/app/shell/handlers-speech.ts +43 -49
  27. package/runtime/app/shell/handlers-system.ts +1 -1
  28. package/runtime/app/shell/handlers.ts +4 -3
  29. package/runtime/app/shell/ios-frameworks.d.ts +9 -0
  30. package/runtime/app/shell/ios-ui-delegate.ts +112 -108
  31. package/runtime/app/shell/native-log.ts +1 -3
  32. package/runtime/app/shell/status-bar.ts +7 -4
  33. package/runtime/app/shell/toast.ts +1 -1
  34. package/runtime/references.d.ts +6 -0
  35. package/runtime/tsconfig.json +1 -0
  36. package/src/cli.ts +43 -18
  37. package/src/derive.ts +8 -0
  38. package/templates/ci/github/workflows/appwrap-pr.yml +3 -3
  39. package/templates/ci/github/workflows/appwrap-release-android.yml +2 -2
  40. package/templates/ci/github/workflows/appwrap-release-ios.yml +2 -2
@@ -9,16 +9,6 @@ import { bridge } from './bridge';
9
9
  * separate handler (Play Billing) and is currently unimplemented (capability 'none').
10
10
  */
11
11
 
12
- declare const SKPaymentQueue: any;
13
- declare const SKPayment: any;
14
- declare const SKProductsRequest: any;
15
- declare const SKProductsRequestDelegate: any;
16
- declare const SKPaymentTransactionObserver: any;
17
- declare const NSSet: any;
18
- declare const NSBundle: any;
19
- declare const NSData: any;
20
- declare const NSNumberFormatter: any;
21
-
22
12
  const PERIOD_UNIT = ['D', 'W', 'M', 'Y']; // SKProductPeriodUnit: Day/Week/Month/Year
23
13
  const STATE = { Purchasing: 0, Purchased: 1, Failed: 2, Restored: 3, Deferred: 4 };
24
14
  const SK_ERR_CANCELLED = 2; // SKErrorPaymentCancelled
@@ -35,10 +25,11 @@ function appReceipt(): string {
35
25
  const url = NSBundle.mainBundle.appStoreReceiptURL;
36
26
  if (!url) return '';
37
27
  const data = NSData.dataWithContentsOfURL(url);
38
- return data ? data.base64EncodedStringWithOptions(0) : '';
28
+ // 0 = NSDataBase64Encoding default (no options); enum-cast for the typed selector.
29
+ return data ? data.base64EncodedStringWithOptions(0 as unknown as NSDataBase64EncodingOptions) : '';
39
30
  }
40
31
 
41
- function mapProduct(p: any) {
32
+ function mapProduct(p: SKProduct) {
42
33
  const f = NSNumberFormatter.new();
43
34
  f.numberStyle = 2; // CurrencyStyle
44
35
  f.locale = p.priceLocale;
@@ -48,7 +39,8 @@ function mapProduct(p: any) {
48
39
  title: String(p.localizedTitle ?? ''),
49
40
  description: String(p.localizedDescription ?? ''),
50
41
  price: p.price ? p.price.doubleValue : 0,
51
- displayPrice: String(f.stringFromNumber(p.price) ?? ''),
42
+ // stringFromNumber takes an NSNumber* at runtime; the .d.ts types it as number, so cast the NSDecimalNumber.
43
+ displayPrice: String(f.stringFromNumber(p.price as unknown as number) ?? ''),
52
44
  currency: String(p.priceLocale?.currencyCode ?? ''),
53
45
  type: sp ? 'autoRenewable' : 'unknown',
54
46
  subscriptionPeriod: sp ? `P${sp.numberOfUnits}${PERIOD_UNIT[sp.unit] ?? 'D'}` : undefined,
@@ -62,82 +54,98 @@ function receiptFor(productId: string, transactionId?: string) {
62
54
  // In-flight resolvers, keyed so the single queue observer can route callbacks.
63
55
  const purchaseWaiters = new Map<string, { resolve: (r: any) => void; reject: (e: any) => void }>();
64
56
  let restoreWaiter: { receipts: any[]; resolve: (r: any[]) => void; reject: (e: any) => void } | null = null;
65
- const productRequests = new Set<any>(); // keep strong refs to in-flight requests + delegates
66
- let observer: any = null;
67
-
68
- function buildObserver() {
69
- const ObserverClass = (NSObject as any).extend(
70
- {
71
- paymentQueueUpdatedTransactions(queue: any, transactions: any): void {
72
- const n = transactions.count;
73
- for (let i = 0; i < n; i++) {
74
- const t = transactions.objectAtIndex(i);
75
- const pid = String(t.payment.productIdentifier);
76
- switch (t.transactionState) {
77
- case STATE.Purchased: {
78
- queue.finishTransaction(t);
79
- const r = receiptFor(pid, String(t.transactionIdentifier ?? ''));
80
- bridge.emit('billing.transaction', r);
81
- purchaseWaiters.get(pid)?.resolve(r);
82
- purchaseWaiters.delete(pid);
83
- break;
84
- }
85
- case STATE.Restored: {
86
- queue.finishTransaction(t);
87
- const r = receiptFor(pid, String(t.transactionIdentifier ?? ''));
88
- if (restoreWaiter) restoreWaiter.receipts.push(r);
89
- bridge.emit('billing.transaction', r);
90
- break;
91
- }
92
- case STATE.Failed: {
93
- queue.finishTransaction(t);
94
- const cancelled = t.error && t.error.code === SK_ERR_CANCELLED;
95
- const e = err(cancelled ? 'DENIED' : 'NATIVE_ERROR', t.error?.localizedDescription ?? 'Purchase failed');
96
- purchaseWaiters.get(pid)?.reject(e);
97
- purchaseWaiters.delete(pid);
98
- break;
99
- }
100
- // Purchasing / Deferred: nothing to do.
101
- }
57
+ const productRequests = new Set<{ request: SKProductsRequest; delegate: ProductsDelegate }>(); // keep strong refs to in-flight requests + delegates
58
+ let observer: PaymentObserver | null = null;
59
+
60
+ // SKPaymentQueue transaction observer. State (purchaseWaiters/restoreWaiter) lives at module
61
+ // scope, so the delegate needs no instance fields.
62
+ @NativeClass()
63
+ class PaymentObserver extends NSObject implements SKPaymentTransactionObserver {
64
+ static ObjCProtocols = [SKPaymentTransactionObserver];
65
+ static new(): PaymentObserver {
66
+ return <PaymentObserver>super.new();
67
+ }
68
+ paymentQueueUpdatedTransactions(queue: SKPaymentQueue, transactions: NSArray<SKPaymentTransaction>): void {
69
+ const n = transactions.count;
70
+ for (let i = 0; i < n; i++) {
71
+ const t = transactions.objectAtIndex(i);
72
+ const pid = String(t.payment.productIdentifier);
73
+ switch (t.transactionState) {
74
+ case STATE.Purchased: {
75
+ queue.finishTransaction(t);
76
+ const r = receiptFor(pid, String(t.transactionIdentifier ?? ''));
77
+ bridge.emit('billing.transaction', r);
78
+ purchaseWaiters.get(pid)?.resolve(r);
79
+ purchaseWaiters.delete(pid);
80
+ break;
81
+ }
82
+ case STATE.Restored: {
83
+ queue.finishTransaction(t);
84
+ const r = receiptFor(pid, String(t.transactionIdentifier ?? ''));
85
+ if (restoreWaiter) restoreWaiter.receipts.push(r);
86
+ bridge.emit('billing.transaction', r);
87
+ break;
102
88
  }
103
- },
104
- paymentQueueRestoreCompletedTransactionsFinished(): void {
105
- restoreWaiter?.resolve(restoreWaiter.receipts);
106
- restoreWaiter = null;
107
- },
108
- paymentQueueRestoreCompletedTransactionsFailedWithError(_q: any, error: any): void {
109
- restoreWaiter?.reject(err('NATIVE_ERROR', error?.localizedDescription ?? 'Restore failed'));
110
- restoreWaiter = null;
111
- },
112
- },
113
- { protocols: [SKPaymentTransactionObserver] }
114
- );
115
- return ObserverClass.new();
89
+ case STATE.Failed: {
90
+ queue.finishTransaction(t);
91
+ const cancelled = t.error && t.error.code === SK_ERR_CANCELLED;
92
+ const e = err(cancelled ? 'DENIED' : 'NATIVE_ERROR', t.error?.localizedDescription ?? 'Purchase failed');
93
+ purchaseWaiters.get(pid)?.reject(e);
94
+ purchaseWaiters.delete(pid);
95
+ break;
96
+ }
97
+ // Purchasing / Deferred: nothing to do.
98
+ }
99
+ }
100
+ }
101
+ paymentQueueRestoreCompletedTransactionsFinished(_queue: SKPaymentQueue): void {
102
+ restoreWaiter?.resolve(restoreWaiter.receipts);
103
+ restoreWaiter = null;
104
+ }
105
+ paymentQueueRestoreCompletedTransactionsFailedWithError(_queue: SKPaymentQueue, error: NSError): void {
106
+ restoreWaiter?.reject(err('NATIVE_ERROR', error?.localizedDescription ?? 'Restore failed'));
107
+ restoreWaiter = null;
108
+ }
109
+ }
110
+
111
+ function buildObserver(): PaymentObserver {
112
+ return PaymentObserver.new();
113
+ }
114
+
115
+ // SKProductsRequest delegate. Closure-captured state (resolve/reject + the strong-ref holder)
116
+ // is held as instance fields, assigned after new().
117
+ @NativeClass()
118
+ class ProductsDelegate extends NSObject implements SKProductsRequestDelegate {
119
+ static ObjCProtocols = [SKProductsRequestDelegate];
120
+ onResolve?: (products: SKProduct[]) => void;
121
+ onReject?: (e: Error) => void;
122
+ holder?: { request: SKProductsRequest; delegate: ProductsDelegate };
123
+ static new(): ProductsDelegate {
124
+ return <ProductsDelegate>super.new();
125
+ }
126
+ productsRequestDidReceiveResponse(_request: SKProductsRequest, response: SKProductsResponse): void {
127
+ const out: SKProduct[] = [];
128
+ const products = response.products;
129
+ for (let i = 0; i < products.count; i++) out.push(products.objectAtIndex(i));
130
+ if (this.holder) productRequests.delete(this.holder);
131
+ this.onResolve?.(out);
132
+ }
133
+ requestDidFailWithError(_request: SKRequest, error: NSError): void {
134
+ if (this.holder) productRequests.delete(this.holder);
135
+ this.onReject?.(err('NATIVE_ERROR', error?.localizedDescription ?? 'Products request failed'));
136
+ }
116
137
  }
117
138
 
118
139
  /** One SKProductsRequest → resolves the raw SKProduct objects (used by both products + purchase). */
119
- function requestSKProducts(ids: string[]): Promise<any[]> {
140
+ function requestSKProducts(ids: string[]): Promise<SKProduct[]> {
120
141
  return new Promise((resolve, reject) => {
121
- const set = NSSet.setWithArray(ids as any);
142
+ const set = NSSet.setWithArray<string>(ids);
122
143
  const request = SKProductsRequest.alloc().initWithProductIdentifiers(set);
123
- const DelegateClass = (NSObject as any).extend(
124
- {
125
- productsRequestDidReceiveResponse(_req: any, response: any): void {
126
- const out: any[] = [];
127
- const products = response.products;
128
- for (let i = 0; i < products.count; i++) out.push(products.objectAtIndex(i));
129
- productRequests.delete(holder);
130
- resolve(out);
131
- },
132
- requestDidFailWithError(_req: any, error: any): void {
133
- productRequests.delete(holder);
134
- reject(err('NATIVE_ERROR', error?.localizedDescription ?? 'Products request failed'));
135
- },
136
- },
137
- { protocols: [SKProductsRequestDelegate] }
138
- );
139
- const delegate = DelegateClass.new();
144
+ const delegate = ProductsDelegate.new();
140
145
  const holder = { request, delegate };
146
+ delegate.onResolve = resolve;
147
+ delegate.onReject = reject;
148
+ delegate.holder = holder;
141
149
  productRequests.add(holder); // strong ref until the callback fires
142
150
  request.delegate = delegate;
143
151
  request.start();
@@ -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