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