@livx.cc/appwrap 0.39.4 → 0.39.8
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/res/values-v29/styles.xml +4 -0
- package/runtime/app/shell/handlers-android.ts +13 -6
- package/runtime/app/shell/handlers-apple-signin.ts +75 -63
- package/runtime/app/shell/handlers-billing.ts +84 -72
- package/runtime/app/shell/handlers-extended.ts +117 -103
- package/runtime/app/shell/handlers-fs.ts +41 -31
- package/runtime/app/shell/handlers-oauth.ts +22 -12
- package/runtime/app/shell/handlers-parity.ts +81 -68
- package/runtime/app/shell/handlers-scanner.ts +26 -16
- package/runtime/app/shell/handlers-speech.ts +20 -17
- package/runtime/app/shell/handlers-system.ts +18 -8
- package/runtime/app/shell/ios-ui-delegate.ts +117 -106
- package/runtime/app/shell/status-bar.ts +40 -8
- package/src/cli.ts +83 -11
- package/src/config.ts +21 -0
|
@@ -7,116 +7,128 @@ import { maskForLock, setIosOrientationMask } from './orientation';
|
|
|
7
7
|
|
|
8
8
|
interface GeoResult { lat: number; lng: number; accuracy: number; }
|
|
9
9
|
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const st = m.authorizationStatus;
|
|
38
|
-
if (st === CLAuthorizationStatus.kCLAuthorizationStatusAuthorizedWhenInUse ||
|
|
39
|
-
st === CLAuthorizationStatus.kCLAuthorizationStatusAuthorizedAlways) {
|
|
40
|
-
m.startUpdatingLocation();
|
|
41
|
-
} else if (st === CLAuthorizationStatus.kCLAuthorizationStatusDenied) {
|
|
10
|
+
// iOS-only native delegates. Defined lazily inside an isIOS-gated factory (mirrors banner.ts) so the
|
|
11
|
+
// shared module can instantiate on Android — NSObject/CL*/PH*/UN* are iOS globals, and a top-level
|
|
12
|
+
// `extends NSObject` would evaluate at ES-module load and crash the Android shell (ReferenceError).
|
|
13
|
+
// any: module-level holders for runtime-built ObjC subclasses; each class BODY stays fully typed.
|
|
14
|
+
let GeoDelegate: any, PhotoPickerDelegate: any, ForegroundNotificationDelegate: any;
|
|
15
|
+
function ensureIosDelegates(): void {
|
|
16
|
+
if (!isIOS || GeoDelegate) return;
|
|
17
|
+
|
|
18
|
+
// CLLocationManager delegate. Closure-captured callbacks (resolve/reject/cleanup) are assigned as
|
|
19
|
+
// instance fields after new() — same pattern as handlers-speech/oauth.
|
|
20
|
+
@NativeClass()
|
|
21
|
+
class GeoDelegateImpl extends NSObject implements CLLocationManagerDelegate {
|
|
22
|
+
static ObjCProtocols = [CLLocationManagerDelegate];
|
|
23
|
+
onResult?: (r: GeoResult) => void;
|
|
24
|
+
onError?: (e: Error) => void;
|
|
25
|
+
onCleanup?: () => void;
|
|
26
|
+
static new(): GeoDelegateImpl {
|
|
27
|
+
return <GeoDelegateImpl>super.new();
|
|
28
|
+
}
|
|
29
|
+
locationManagerDidUpdateLocations(_m: CLLocationManager, locations: NSArray<CLLocation>): void {
|
|
30
|
+
const loc = locations.lastObject;
|
|
31
|
+
if (!loc) return;
|
|
32
|
+
const result = {
|
|
33
|
+
lat: loc.coordinate.latitude,
|
|
34
|
+
lng: loc.coordinate.longitude,
|
|
35
|
+
accuracy: loc.horizontalAccuracy,
|
|
36
|
+
};
|
|
42
37
|
this.onCleanup?.();
|
|
43
|
-
this.
|
|
38
|
+
this.onResult?.(result);
|
|
39
|
+
}
|
|
40
|
+
locationManagerDidFailWithError(_m: CLLocationManager, error: NSError): void {
|
|
41
|
+
this.onCleanup?.();
|
|
42
|
+
this.onError?.(Object.assign(new Error(error.localizedDescription), { code: 'DENIED' }));
|
|
43
|
+
}
|
|
44
|
+
locationManagerDidChangeAuthorization(m: CLLocationManager): void {
|
|
45
|
+
const st = m.authorizationStatus;
|
|
46
|
+
if (st === CLAuthorizationStatus.kCLAuthorizationStatusAuthorizedWhenInUse ||
|
|
47
|
+
st === CLAuthorizationStatus.kCLAuthorizationStatusAuthorizedAlways) {
|
|
48
|
+
m.startUpdatingLocation();
|
|
49
|
+
} else if (st === CLAuthorizationStatus.kCLAuthorizationStatusDenied) {
|
|
50
|
+
this.onCleanup?.();
|
|
51
|
+
this.onError?.(Object.assign(new Error('location permission denied'), { code: 'DENIED' }));
|
|
52
|
+
}
|
|
44
53
|
}
|
|
45
54
|
}
|
|
46
|
-
|
|
55
|
+
GeoDelegate = GeoDelegateImpl;
|
|
47
56
|
|
|
48
|
-
// PHPickerViewController delegate. Closure-captured opts (dataUrl/maxSize) + resolve assigned post-new().
|
|
49
|
-
@NativeClass()
|
|
50
|
-
class
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
57
|
+
// PHPickerViewController delegate. Closure-captured opts (dataUrl/maxSize) + resolve assigned post-new().
|
|
58
|
+
@NativeClass()
|
|
59
|
+
class PhotoPickerDelegateImpl extends NSObject implements PHPickerViewControllerDelegate {
|
|
60
|
+
static ObjCProtocols = [PHPickerViewControllerDelegate];
|
|
61
|
+
wantDataUrl?: boolean;
|
|
62
|
+
maxSize?: number;
|
|
63
|
+
onResult?: (r: { picked: boolean; width?: number; height?: number; dataUrl?: string }) => void;
|
|
64
|
+
static new(): PhotoPickerDelegateImpl {
|
|
65
|
+
return <PhotoPickerDelegateImpl>super.new();
|
|
66
|
+
}
|
|
67
|
+
pickerDidFinishPicking(picker: PHPickerViewController, results: NSArray<PHPickerResult>): void {
|
|
68
|
+
picker.dismissViewControllerAnimatedCompletion(true, null);
|
|
69
|
+
// Recover the WebView after the picker dismisses (orphaned-window / renderer-throttle guard).
|
|
70
|
+
bridge.getWebView()?.recoverAfterNativeSurface();
|
|
71
|
+
const result = results.count > 0 ? results.objectAtIndex(0) : null;
|
|
72
|
+
if (!result) return this.onResult?.({ picked: false });
|
|
73
|
+
result.itemProvider.loadObjectOfClassCompletionHandler(UIImage.class(), (img: UIImage) => {
|
|
74
|
+
if (!img) return this.onResult?.({ picked: true });
|
|
75
|
+
const out: { picked: boolean; width?: number; height?: number; dataUrl?: string } =
|
|
76
|
+
{ picked: true, width: img.size.width, height: img.size.height };
|
|
77
|
+
if (this.wantDataUrl) out.dataUrl = uiImageToDataUrl(img, this.maxSize ?? 1024);
|
|
78
|
+
this.onResult?.(out);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
71
81
|
}
|
|
72
|
-
|
|
82
|
+
PhotoPickerDelegate = PhotoPickerDelegateImpl;
|
|
73
83
|
|
|
74
|
-
// UNUserNotificationCenter delegate — foreground present + tap routing. Idempotent singleton (notifDelegate).
|
|
75
|
-
@NativeClass()
|
|
76
|
-
class
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
// Notification tapped → route its deep link through the same path as an external open.
|
|
97
|
-
userNotificationCenterDidReceiveNotificationResponseWithCompletionHandler(
|
|
98
|
-
_center: UNUserNotificationCenter,
|
|
99
|
-
response: UNNotificationResponse,
|
|
100
|
-
completionHandler: () => void
|
|
101
|
-
): void {
|
|
102
|
-
// Tapped REMOTE push → kit.push.onTap (with payload). Local notifs keep the deep-link path below.
|
|
103
|
-
if (isRemotePush(response.notification)) onRemoteMessage(response.notification.request.content.userInfo, true);
|
|
104
|
-
// userInfo may come back as an NSDictionary or an auto-marshalled JS object.
|
|
105
|
-
// any: dual-path payload (NSDictionary vs marshalled JS object) probed dynamically.
|
|
106
|
-
const info: any = response.notification.request.content.userInfo;
|
|
107
|
-
const url = info ? (typeof info.objectForKey === 'function' ? info.objectForKey('url') : info.url) : null;
|
|
108
|
-
// Diagnostic breadcrumb (persists across the cold relaunch) — surfaced in the handshake's debug field.
|
|
109
|
-
try {
|
|
110
|
-
ApplicationSettings.setString(
|
|
111
|
-
'kit:__notifTap',
|
|
112
|
-
JSON.stringify({ at: Date.now(), url: url ? String(url) : null, hadInfo: !!info })
|
|
84
|
+
// UNUserNotificationCenter delegate — foreground present + tap routing. Idempotent singleton (notifDelegate).
|
|
85
|
+
@NativeClass()
|
|
86
|
+
class ForegroundNotificationDelegateImpl extends NSObject implements UNUserNotificationCenterDelegate {
|
|
87
|
+
static ObjCProtocols = [UNUserNotificationCenterDelegate];
|
|
88
|
+
static new(): ForegroundNotificationDelegateImpl {
|
|
89
|
+
return <ForegroundNotificationDelegateImpl>super.new();
|
|
90
|
+
}
|
|
91
|
+
userNotificationCenterWillPresentNotificationWithCompletionHandler(
|
|
92
|
+
_center: UNUserNotificationCenter,
|
|
93
|
+
notification: UNNotification,
|
|
94
|
+
completionHandler: (opts: UNNotificationPresentationOptions) => void
|
|
95
|
+
): void {
|
|
96
|
+
// Foreground REMOTE push (push trigger) → surface to kit.push.onMessage.
|
|
97
|
+
if (isRemotePush(notification)) onRemoteMessage(notification.request.content.userInfo, false);
|
|
98
|
+
// Present banner + list + sound + badge even while foreground (min target iOS 16).
|
|
99
|
+
completionHandler(
|
|
100
|
+
UNNotificationPresentationOptions.Banner |
|
|
101
|
+
UNNotificationPresentationOptions.List |
|
|
102
|
+
UNNotificationPresentationOptions.Sound |
|
|
103
|
+
UNNotificationPresentationOptions.Badge
|
|
113
104
|
);
|
|
114
|
-
} catch {
|
|
115
|
-
/* diagnostic only */
|
|
116
105
|
}
|
|
117
|
-
|
|
118
|
-
|
|
106
|
+
// Notification tapped → route its deep link through the same path as an external open.
|
|
107
|
+
userNotificationCenterDidReceiveNotificationResponseWithCompletionHandler(
|
|
108
|
+
_center: UNUserNotificationCenter,
|
|
109
|
+
response: UNNotificationResponse,
|
|
110
|
+
completionHandler: () => void
|
|
111
|
+
): void {
|
|
112
|
+
// Tapped REMOTE push → kit.push.onTap (with payload). Local notifs keep the deep-link path below.
|
|
113
|
+
if (isRemotePush(response.notification)) onRemoteMessage(response.notification.request.content.userInfo, true);
|
|
114
|
+
// userInfo may come back as an NSDictionary or an auto-marshalled JS object.
|
|
115
|
+
// any: dual-path payload (NSDictionary vs marshalled JS object) probed dynamically.
|
|
116
|
+
const info: any = response.notification.request.content.userInfo;
|
|
117
|
+
const url = info ? (typeof info.objectForKey === 'function' ? info.objectForKey('url') : info.url) : null;
|
|
118
|
+
// Diagnostic breadcrumb (persists across the cold relaunch) — surfaced in the handshake's debug field.
|
|
119
|
+
try {
|
|
120
|
+
ApplicationSettings.setString(
|
|
121
|
+
'kit:__notifTap',
|
|
122
|
+
JSON.stringify({ at: Date.now(), url: url ? String(url) : null, hadInfo: !!info })
|
|
123
|
+
);
|
|
124
|
+
} catch {
|
|
125
|
+
/* diagnostic only */
|
|
126
|
+
}
|
|
127
|
+
if (url) onDeepLink(String(url));
|
|
128
|
+
completionHandler();
|
|
129
|
+
}
|
|
119
130
|
}
|
|
131
|
+
ForegroundNotificationDelegate = ForegroundNotificationDelegateImpl;
|
|
120
132
|
}
|
|
121
133
|
|
|
122
134
|
/**
|
|
@@ -124,6 +136,7 @@ class ForegroundNotificationDelegate extends NSObject implements UNUserNotificat
|
|
|
124
136
|
* Each handler is small and self-contained; heavy domains (push, files) come later.
|
|
125
137
|
*/
|
|
126
138
|
export function registerExtendedHandlers(): void {
|
|
139
|
+
ensureIosDelegates(); // no-op on Android; defines the iOS delegate classes used by the handlers below
|
|
127
140
|
// ── device ─────────────────────────────────────────────────────────
|
|
128
141
|
bridge.register('device.info', () => {
|
|
129
142
|
let battery: { level: number; charging: boolean } | undefined;
|
|
@@ -298,7 +311,7 @@ export function registerExtendedHandlers(): void {
|
|
|
298
311
|
}, 15000);
|
|
299
312
|
|
|
300
313
|
let manager: CLLocationManager | null = null;
|
|
301
|
-
let delegate:
|
|
314
|
+
let delegate: any = null;
|
|
302
315
|
const cleanup = () => {
|
|
303
316
|
clearTimeout(timer);
|
|
304
317
|
manager?.stopUpdatingLocation();
|
|
@@ -406,7 +419,7 @@ function applyIosOrientation(mask: number): void {
|
|
|
406
419
|
});
|
|
407
420
|
}
|
|
408
421
|
|
|
409
|
-
let notifDelegate:
|
|
422
|
+
let notifDelegate: any = null; // retained for the app's lifetime
|
|
410
423
|
/**
|
|
411
424
|
* Install the UNUserNotificationCenter delegate. MUST run before
|
|
412
425
|
* `didFinishLaunching` returns, else a tap that COLD-LAUNCHES the app never
|
|
@@ -416,6 +429,7 @@ let notifDelegate: ForegroundNotificationDelegate | null = null; // retained for
|
|
|
416
429
|
*/
|
|
417
430
|
export function installForegroundNotificationDelegate(): void {
|
|
418
431
|
if (!isIOS || notifDelegate) return;
|
|
432
|
+
ensureIosDelegates();
|
|
419
433
|
notifDelegate = ForegroundNotificationDelegate.new();
|
|
420
434
|
UNUserNotificationCenter.currentNotificationCenter().delegate = notifDelegate;
|
|
421
435
|
}
|
|
@@ -152,42 +152,52 @@ function iosStat(full: string, name: string): any {
|
|
|
152
152
|
|
|
153
153
|
/**
|
|
154
154
|
* Delegate for UIDocumentPickerViewController. Captures the resolve callback as an instance field
|
|
155
|
-
* (the original closed over `resolve`); both selectors funnel into it.
|
|
155
|
+
* (the original closed over `resolve`); both selectors funnel into it. Built lazily INSIDE the
|
|
156
|
+
* iOS-only path (mirrors banner.ts): NSObject/UI* are iOS globals and this file is imported on Android
|
|
157
|
+
* too, so a top-level `@NativeClass extends NSObject` would crash at module load.
|
|
156
158
|
*/
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
159
|
+
// any: runtime-built ObjC subclass holder; the class BODY stays fully typed.
|
|
160
|
+
let DocumentPickerDelegate: any;
|
|
161
|
+
function documentPickerDelegateClass(): any {
|
|
162
|
+
if (!DocumentPickerDelegate) {
|
|
163
|
+
@NativeClass()
|
|
164
|
+
class DocumentPickerDelegateImpl extends NSObject implements UIDocumentPickerDelegate {
|
|
165
|
+
static ObjCProtocols = [UIDocumentPickerDelegate];
|
|
166
|
+
onPick!: (out: any[]) => void;
|
|
167
|
+
|
|
168
|
+
static new(): DocumentPickerDelegateImpl {
|
|
169
|
+
return <DocumentPickerDelegateImpl>super.new();
|
|
170
|
+
}
|
|
165
171
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
172
|
+
documentPickerDidPickDocumentsAtURLs(_controller: UIDocumentPickerViewController, urls: NSArray<NSURL>): void {
|
|
173
|
+
const out: any[] = [];
|
|
174
|
+
for (let i = 0; i < urls.count; i++) {
|
|
175
|
+
const url = urls.objectAtIndex(i);
|
|
176
|
+
const scoped = url.startAccessingSecurityScopedResource?.();
|
|
177
|
+
try {
|
|
178
|
+
const data = NSData.dataWithContentsOfURL(url);
|
|
179
|
+
if (data) {
|
|
180
|
+
out.push({
|
|
181
|
+
name: url.lastPathComponent,
|
|
182
|
+
mimeType: mimeForUrlIos(url),
|
|
183
|
+
size: data.length,
|
|
184
|
+
base64: data.base64EncodedStringWithOptions(0 as unknown as NSDataBase64EncodingOptions),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
} finally {
|
|
188
|
+
if (scoped) url.stopAccessingSecurityScopedResource?.();
|
|
189
|
+
}
|
|
180
190
|
}
|
|
181
|
-
|
|
182
|
-
if (scoped) url.stopAccessingSecurityScopedResource?.();
|
|
191
|
+
this.onPick(out);
|
|
183
192
|
}
|
|
184
|
-
}
|
|
185
|
-
this.onPick(out);
|
|
186
|
-
}
|
|
187
193
|
|
|
188
|
-
|
|
189
|
-
|
|
194
|
+
documentPickerWasCancelled(_controller: UIDocumentPickerViewController): void {
|
|
195
|
+
this.onPick([]);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
DocumentPickerDelegate = DocumentPickerDelegateImpl;
|
|
190
199
|
}
|
|
200
|
+
return DocumentPickerDelegate;
|
|
191
201
|
}
|
|
192
202
|
|
|
193
203
|
/**
|
|
@@ -211,7 +221,7 @@ function pickFileIos(types: string[] | undefined, multiple: boolean): Promise<an
|
|
|
211
221
|
const picker = UIDocumentPickerViewController.alloc().initForOpeningContentTypes(utis as any);
|
|
212
222
|
picker.allowsMultipleSelection = multiple;
|
|
213
223
|
|
|
214
|
-
const delegate =
|
|
224
|
+
const delegate = documentPickerDelegateClass().new();
|
|
215
225
|
delegate.onPick = resolve;
|
|
216
226
|
(picker as any)._appwrapDelegate = delegate; // interop: retain delegate past the present call (no typed slot)
|
|
217
227
|
picker.delegate = delegate;
|
|
@@ -11,18 +11,28 @@ const err = (code: string, message: string) => Object.assign(new Error(message),
|
|
|
11
11
|
let activeSession: ASWebAuthenticationSession | null = null;
|
|
12
12
|
let activeProvider: ASWebAuthenticationPresentationContextProviding | null = null;
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
14
|
+
// Built lazily INSIDE the iOS-only path (mirrors banner.ts): NSObject/AS* are iOS globals and this file
|
|
15
|
+
// is imported on Android too, so a top-level `@NativeClass extends NSObject` would crash at module load.
|
|
16
|
+
// any: runtime-built ObjC subclass holder; the class BODY stays fully typed.
|
|
17
|
+
let OAuthPresentationContextProvider: any;
|
|
18
|
+
function oauthContextProviderClass(): any {
|
|
19
|
+
if (!OAuthPresentationContextProvider) {
|
|
20
|
+
@NativeClass()
|
|
21
|
+
class OAuthPresentationContextProviderImpl extends NSObject implements ASWebAuthenticationPresentationContextProviding {
|
|
22
|
+
static ObjCProtocols = [ASWebAuthenticationPresentationContextProviding];
|
|
23
|
+
static new(): OAuthPresentationContextProviderImpl {
|
|
24
|
+
return <OAuthPresentationContextProviderImpl>super.new();
|
|
25
|
+
}
|
|
26
|
+
presentationAnchorForWebAuthenticationSession(_session: ASWebAuthenticationSession): UIWindow {
|
|
27
|
+
return (
|
|
28
|
+
Utils.ios.getRootViewController()?.view?.window ??
|
|
29
|
+
UIApplication.sharedApplication.keyWindow
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
OAuthPresentationContextProvider = OAuthPresentationContextProviderImpl;
|
|
25
34
|
}
|
|
35
|
+
return OAuthPresentationContextProvider;
|
|
26
36
|
}
|
|
27
37
|
|
|
28
38
|
/**
|
|
@@ -64,7 +74,7 @@ export function registerOAuthHandlers(): void {
|
|
|
64
74
|
}
|
|
65
75
|
);
|
|
66
76
|
|
|
67
|
-
activeProvider =
|
|
77
|
+
activeProvider = oauthContextProviderClass().new();
|
|
68
78
|
session.presentationContextProvider = activeProvider;
|
|
69
79
|
session.prefersEphemeralWebBrowserSession = !!ephemeral;
|
|
70
80
|
activeSession = session;
|
|
@@ -20,77 +20,89 @@ function readContact(contact: CNContact): { name: string; phones: string[]; emai
|
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
23
|
+
// iOS-only native delegates. Defined lazily inside an isIOS-gated factory (mirrors banner.ts) so the
|
|
24
|
+
// shared module can instantiate on Android — NSObject/CL*/CN*/UIImagePicker* are iOS globals, and a
|
|
25
|
+
// top-level `extends NSObject` would evaluate at ES-module load and crash the Android shell.
|
|
26
|
+
// any: module-level holders for runtime-built ObjC subclasses; each class BODY stays fully typed.
|
|
27
|
+
let GeoWatchDelegate: any, ContactPickerDelegate: any, CameraCaptureDelegate: any;
|
|
28
|
+
function ensureIosDelegates(): void {
|
|
29
|
+
if (!isIOS || GeoWatchDelegate) return;
|
|
30
|
+
|
|
31
|
+
// CLLocationManager delegate for the persistent geo.watch stream. Callbacks are wired via instance
|
|
32
|
+
// fields so the closure-captured behavior of the old (NSObject).extend(...) is preserved.
|
|
33
|
+
@NativeClass()
|
|
34
|
+
class GeoWatchDelegateImpl extends NSObject implements CLLocationManagerDelegate {
|
|
35
|
+
static ObjCProtocols = [CLLocationManagerDelegate];
|
|
36
|
+
static new(): GeoWatchDelegateImpl {
|
|
37
|
+
return <GeoWatchDelegateImpl>super.new();
|
|
38
|
+
}
|
|
39
|
+
locationManagerDidUpdateLocations(_m: CLLocationManager, locations: NSArray<CLLocation>): void {
|
|
40
|
+
const loc = locations.lastObject;
|
|
41
|
+
if (!loc) return;
|
|
42
|
+
bridge.emit('geo.position', {
|
|
43
|
+
lat: loc.coordinate.latitude,
|
|
44
|
+
lng: loc.coordinate.longitude,
|
|
45
|
+
accuracy: loc.horizontalAccuracy,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
locationManagerDidFailWithError(_m: CLLocationManager, error: NSError): void {
|
|
49
|
+
console.warn('AppWrap: geo.watch error', error.localizedDescription);
|
|
50
|
+
}
|
|
51
|
+
locationManagerDidChangeAuthorization(m: CLLocationManager): void {
|
|
52
|
+
const st = m.authorizationStatus;
|
|
53
|
+
if (st === CLAuthorizationStatus.kCLAuthorizationStatusAuthorizedWhenInUse ||
|
|
54
|
+
st === CLAuthorizationStatus.kCLAuthorizationStatusAuthorizedAlways) {
|
|
55
|
+
m.startUpdatingLocation();
|
|
56
|
+
}
|
|
48
57
|
}
|
|
49
58
|
}
|
|
50
|
-
|
|
59
|
+
GeoWatchDelegate = GeoWatchDelegateImpl;
|
|
51
60
|
|
|
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
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
// CNContactPickerViewController delegate. The picker resolution callbacks are set as instance fields
|
|
62
|
+
// after construction (replacing the old closure-captured `resolve`).
|
|
63
|
+
@NativeClass()
|
|
64
|
+
class ContactPickerDelegateImpl extends NSObject implements CNContactPickerDelegate {
|
|
65
|
+
static ObjCProtocols = [CNContactPickerDelegate];
|
|
66
|
+
static new(): ContactPickerDelegateImpl {
|
|
67
|
+
return <ContactPickerDelegateImpl>super.new();
|
|
68
|
+
}
|
|
69
|
+
onSelect?: (contact: CNContact) => void;
|
|
70
|
+
onCancel?: (picker: CNContactPickerViewController) => void;
|
|
71
|
+
contactPickerDidSelectContact(_picker: CNContactPickerViewController, contact: CNContact): void {
|
|
72
|
+
this.onSelect?.(contact);
|
|
73
|
+
}
|
|
74
|
+
contactPickerDidCancel(picker: CNContactPickerViewController): void {
|
|
75
|
+
this.onCancel?.(picker);
|
|
76
|
+
}
|
|
67
77
|
}
|
|
68
|
-
|
|
78
|
+
ContactPickerDelegate = ContactPickerDelegateImpl;
|
|
69
79
|
|
|
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
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
80
|
+
// UIImagePickerController delegate (camera.capture). Result/options are set as instance fields after
|
|
81
|
+
// construction, replacing the old closure-captured `resolve`/`dataUrl`/`maxSize`.
|
|
82
|
+
@NativeClass()
|
|
83
|
+
class CameraCaptureDelegateImpl extends NSObject implements UIImagePickerControllerDelegate, UINavigationControllerDelegate {
|
|
84
|
+
static ObjCProtocols = [UIImagePickerControllerDelegate, UINavigationControllerDelegate];
|
|
85
|
+
static new(): CameraCaptureDelegateImpl {
|
|
86
|
+
return <CameraCaptureDelegateImpl>super.new();
|
|
87
|
+
}
|
|
88
|
+
wantDataUrl = false;
|
|
89
|
+
maxSize = 1024;
|
|
90
|
+
onResult?: (out: { picked: boolean; width?: number; height?: number; dataUrl?: string }) => void;
|
|
91
|
+
imagePickerControllerDidFinishPickingMediaWithInfo(picker: UIImagePickerController, info: NSDictionary<string, any>): void {
|
|
92
|
+
picker.dismissViewControllerAnimatedCompletion(true, null);
|
|
93
|
+
const img = info.objectForKey(UIImagePickerControllerOriginalImage) as UIImage | null;
|
|
94
|
+
if (!img) return this.onResult?.({ picked: true });
|
|
95
|
+
const out: { picked: boolean; width?: number; height?: number; dataUrl?: string } =
|
|
96
|
+
{ picked: true, width: img.size.width, height: img.size.height };
|
|
97
|
+
if (this.wantDataUrl) out.dataUrl = uiImageToDataUrl(img, this.maxSize);
|
|
98
|
+
this.onResult?.(out);
|
|
99
|
+
}
|
|
100
|
+
imagePickerControllerDidCancel(picker: UIImagePickerController): void {
|
|
101
|
+
picker.dismissViewControllerAnimatedCompletion(true, null);
|
|
102
|
+
this.onResult?.({ picked: false });
|
|
103
|
+
}
|
|
93
104
|
}
|
|
105
|
+
CameraCaptureDelegate = CameraCaptureDelegateImpl;
|
|
94
106
|
}
|
|
95
107
|
|
|
96
108
|
/**
|
|
@@ -98,6 +110,7 @@ class CameraCaptureDelegate extends NSObject implements UIImagePickerControllerD
|
|
|
98
110
|
* motion, contacts, calendar, camera. iOS-first like handlers-extended.
|
|
99
111
|
*/
|
|
100
112
|
export function registerParityHandlers(): void {
|
|
113
|
+
ensureIosDelegates(); // no-op on Android; defines the iOS delegate classes used by the handlers below
|
|
101
114
|
// ── dialogs (NS Dialogs — cross-platform) ──────────────────────────
|
|
102
115
|
bridge.register('ui.alert', ({ title, message, ok }: any) =>
|
|
103
116
|
Dialogs.alert({ title, message: String(message ?? ''), okButtonText: ok ?? 'OK' }).then(() => undefined)
|
|
@@ -139,7 +152,7 @@ export function registerParityHandlers(): void {
|
|
|
139
152
|
|
|
140
153
|
// ── geo.watch (persistent CLLocationManager) ───────────────────────
|
|
141
154
|
let geoManager: CLLocationManager | null = null;
|
|
142
|
-
let geoDelegate:
|
|
155
|
+
let geoDelegate: any = null;
|
|
143
156
|
|
|
144
157
|
bridge.register('geo.watch.start', () => {
|
|
145
158
|
if (!isIOS) throw iosOnly();
|
|
@@ -213,10 +226,10 @@ export function registerParityHandlers(): void {
|
|
|
213
226
|
return new Promise((resolve) => {
|
|
214
227
|
Utils.dispatchToMainThread(() => {
|
|
215
228
|
const delegate = ContactPickerDelegate.new();
|
|
216
|
-
delegate.onSelect = (contact) => {
|
|
229
|
+
delegate.onSelect = (contact: CNContact) => {
|
|
217
230
|
resolve({ picked: true, ...readContact(contact) });
|
|
218
231
|
};
|
|
219
|
-
delegate.onCancel = (picker) => {
|
|
232
|
+
delegate.onCancel = (picker: CNContactPickerViewController) => {
|
|
220
233
|
picker.dismissViewControllerAnimatedCompletion(true, null);
|
|
221
234
|
resolve({ picked: false });
|
|
222
235
|
};
|