@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
|
@@ -1,30 +1,22 @@
|
|
|
1
1
|
import { Utils, isIOS } from '@nativescript/core';
|
|
2
2
|
import { bridge } from './bridge';
|
|
3
3
|
|
|
4
|
-
// AVAudioSession globals (NS resolves these from the AVFoundation metadata at runtime).
|
|
5
|
-
declare const AVAudioSession: any;
|
|
6
|
-
declare const AVAudioSessionCategoryPlayAndRecord: string;
|
|
7
|
-
declare const AVAudioSessionPortBuiltInReceiver: string;
|
|
8
|
-
declare const AVAudioSessionRouteChangeNotification: string;
|
|
9
|
-
declare const AVAudioSessionPortOverride: any;
|
|
10
|
-
declare const NSNotificationCenter: any;
|
|
11
|
-
|
|
12
4
|
let routeObserverArmed = false;
|
|
13
5
|
|
|
14
6
|
/** List the current output port types (e.g. "Receiver", "Speaker", "BluetoothA2DP"). */
|
|
15
|
-
function outputPortTypes(session:
|
|
7
|
+
function outputPortTypes(session: AVAudioSession): string[] {
|
|
16
8
|
const outputs = session.currentRoute?.outputs;
|
|
17
9
|
const types: string[] = [];
|
|
18
10
|
for (let i = 0; i < (outputs?.count ?? 0); i++) types.push(String(outputs.objectAtIndex(i).portType));
|
|
19
11
|
return types;
|
|
20
12
|
}
|
|
21
13
|
|
|
22
|
-
function isOnBuiltInReceiver(session:
|
|
14
|
+
function isOnBuiltInReceiver(session: AVAudioSession): boolean {
|
|
23
15
|
return outputPortTypes(session).indexOf(AVAudioSessionPortBuiltInReceiver) !== -1;
|
|
24
16
|
}
|
|
25
17
|
|
|
26
18
|
/** Whether the session can record (PlayAndRecord) — i.e. we're (or about to be) in a call. */
|
|
27
|
-
function isRecordCategory(session:
|
|
19
|
+
function isRecordCategory(session: AVAudioSession): boolean {
|
|
28
20
|
return String(session.category) === String(AVAudioSessionCategoryPlayAndRecord);
|
|
29
21
|
}
|
|
30
22
|
|
|
@@ -33,7 +25,7 @@ function forceSpeakerIfReceiverDuringCall(): void {
|
|
|
33
25
|
const session = AVAudioSession.sharedInstance();
|
|
34
26
|
if (!isRecordCategory(session) || !isOnBuiltInReceiver(session)) return; // not a call, or already speaker/headset/BT
|
|
35
27
|
try {
|
|
36
|
-
session.overrideOutputAudioPortError(AVAudioSessionPortOverride.Speaker
|
|
28
|
+
session.overrideOutputAudioPortError(AVAudioSessionPortOverride.Speaker);
|
|
37
29
|
} catch {
|
|
38
30
|
/* benign — route may have changed again under us */
|
|
39
31
|
}
|
|
@@ -60,6 +52,7 @@ function observeAudioRouteForSpeaker(): void {
|
|
|
60
52
|
*/
|
|
61
53
|
export function registerMediaHandlers(): void {
|
|
62
54
|
// DIAGNOSTIC: surface the WKWebView UIDelegate status to the page (on-screen log).
|
|
55
|
+
// no NS types: __appwrapWebviewDiag is an ad-hoc diagnostic stashed on global by the WKWebView UIDelegate.
|
|
63
56
|
bridge.register('debug.webviewInfo', () => ({ diag: (global as any).__appwrapWebviewDiag ?? 'n/a (android/web)' }));
|
|
64
57
|
|
|
65
58
|
// Auto loudspeaker for in-WebView WebRTC calls. When getUserMedia is live, WebKit drives the
|
|
@@ -120,7 +113,8 @@ export function registerMediaHandlers(): void {
|
|
|
120
113
|
} else {
|
|
121
114
|
category = AVAudioSessionCategoryPlayback; // ignores the silent switch
|
|
122
115
|
}
|
|
123
|
-
|
|
124
|
-
session.
|
|
116
|
+
// options is an OR-combined flag mask; the bitwise result widens to number, so cast back to the enum.
|
|
117
|
+
session.setCategoryModeOptionsError(category, avMode, options as AVAudioSessionCategoryOptions);
|
|
118
|
+
session.setActiveError(true);
|
|
125
119
|
});
|
|
126
120
|
}
|
|
@@ -2,17 +2,28 @@ import { Application, Utils, isAndroid, isIOS } from '@nativescript/core';
|
|
|
2
2
|
import { bridge } from './bridge';
|
|
3
3
|
import { setDeepLinkInterceptor } from './events';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
declare const ASWebAuthenticationSession: any;
|
|
7
|
-
declare const ASWebAuthenticationPresentationContextProviding: any;
|
|
8
|
-
declare const android: any, androidx: any;
|
|
5
|
+
declare const androidx: any; // no NS types: androidx
|
|
9
6
|
|
|
10
7
|
const err = (code: string, message: string) => Object.assign(new Error(message), { code });
|
|
11
8
|
|
|
12
9
|
// Keep strong refs while the sheet is up — ARC frees the session + its context provider the moment
|
|
13
10
|
// the JS locals fall out of scope, which silently cancels the flow mid-handshake.
|
|
14
|
-
let activeSession:
|
|
15
|
-
let activeProvider:
|
|
11
|
+
let activeSession: ASWebAuthenticationSession | null = null;
|
|
12
|
+
let activeProvider: ASWebAuthenticationPresentationContextProviding | null = null;
|
|
13
|
+
|
|
14
|
+
@NativeClass()
|
|
15
|
+
class OAuthPresentationContextProvider extends NSObject implements ASWebAuthenticationPresentationContextProviding {
|
|
16
|
+
static ObjCProtocols = [ASWebAuthenticationPresentationContextProviding];
|
|
17
|
+
static new(): OAuthPresentationContextProvider {
|
|
18
|
+
return <OAuthPresentationContextProvider>super.new();
|
|
19
|
+
}
|
|
20
|
+
presentationAnchorForWebAuthenticationSession(_session: ASWebAuthenticationSession): UIWindow {
|
|
21
|
+
return (
|
|
22
|
+
Utils.ios.getRootViewController()?.view?.window ??
|
|
23
|
+
UIApplication.sharedApplication.keyWindow
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
16
27
|
|
|
17
28
|
/**
|
|
18
29
|
* System-browser OAuth (iOS ASWebAuthenticationSession). Runs the provider's auth page in the
|
|
@@ -37,7 +48,7 @@ export function registerOAuthHandlers(): void {
|
|
|
37
48
|
const session = ASWebAuthenticationSession.alloc().initWithURLCallbackURLSchemeCompletionHandler(
|
|
38
49
|
NSURL.URLWithString(target),
|
|
39
50
|
scheme,
|
|
40
|
-
(callbackURL:
|
|
51
|
+
(callbackURL: NSURL, error: NSError) => {
|
|
41
52
|
activeSession = null;
|
|
42
53
|
activeProvider = null;
|
|
43
54
|
if (error) {
|
|
@@ -50,18 +61,7 @@ export function registerOAuthHandlers(): void {
|
|
|
50
61
|
}
|
|
51
62
|
);
|
|
52
63
|
|
|
53
|
-
|
|
54
|
-
{
|
|
55
|
-
presentationAnchorForWebAuthenticationSession(_session: any): any {
|
|
56
|
-
return (
|
|
57
|
-
Utils.ios.getRootViewController()?.view?.window ??
|
|
58
|
-
UIApplication.sharedApplication.keyWindow
|
|
59
|
-
);
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
{ protocols: [ASWebAuthenticationPresentationContextProviding] }
|
|
63
|
-
);
|
|
64
|
-
activeProvider = ProviderClass.new();
|
|
64
|
+
activeProvider = OAuthPresentationContextProvider.new();
|
|
65
65
|
session.presentationContextProvider = activeProvider;
|
|
66
66
|
session.prefersEphemeralWebBrowserSession = !!ephemeral;
|
|
67
67
|
activeSession = session;
|
|
@@ -5,6 +5,94 @@ import { uiImageToDataUrl } from './ios-image';
|
|
|
5
5
|
const err = (code: string, message: string) => Object.assign(new Error(message), { code });
|
|
6
6
|
const iosOnly = () => err('UNSUPPORTED', 'iOS only for now');
|
|
7
7
|
|
|
8
|
+
/** Read a CNContact into the kit-contract shape (name + phone/email string arrays). */
|
|
9
|
+
function readContact(contact: CNContact): { name: string; phones: string[]; emails: string[] } {
|
|
10
|
+
// CNLabeledValue<T>.value is typed `any` in the SDK, so the per-value map stays loosely typed.
|
|
11
|
+
const collect = (labeled: NSArray<CNLabeledValue<any>>, map: (v: any) => string): string[] => {
|
|
12
|
+
const out: string[] = [];
|
|
13
|
+
for (let i = 0; i < labeled.count; i++) out.push(map(labeled.objectAtIndex(i).value));
|
|
14
|
+
return out;
|
|
15
|
+
};
|
|
16
|
+
return {
|
|
17
|
+
name: `${contact.givenName ?? ''} ${contact.familyName ?? ''}`.trim(),
|
|
18
|
+
phones: collect(contact.phoneNumbers, (v) => String(v.stringValue)),
|
|
19
|
+
emails: collect(contact.emailAddresses, (v) => String(v)),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// CLLocationManager delegate for the persistent geo.watch stream. Callbacks are wired via instance
|
|
24
|
+
// fields so the closure-captured behavior of the old (NSObject).extend(...) is preserved.
|
|
25
|
+
@NativeClass()
|
|
26
|
+
class GeoWatchDelegate extends NSObject implements CLLocationManagerDelegate {
|
|
27
|
+
static ObjCProtocols = [CLLocationManagerDelegate];
|
|
28
|
+
static new(): GeoWatchDelegate {
|
|
29
|
+
return <GeoWatchDelegate>super.new();
|
|
30
|
+
}
|
|
31
|
+
locationManagerDidUpdateLocations(_m: CLLocationManager, locations: NSArray<CLLocation>): void {
|
|
32
|
+
const loc = locations.lastObject;
|
|
33
|
+
if (!loc) return;
|
|
34
|
+
bridge.emit('geo.position', {
|
|
35
|
+
lat: loc.coordinate.latitude,
|
|
36
|
+
lng: loc.coordinate.longitude,
|
|
37
|
+
accuracy: loc.horizontalAccuracy,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
locationManagerDidFailWithError(_m: CLLocationManager, error: NSError): void {
|
|
41
|
+
console.warn('AppWrap: geo.watch error', error.localizedDescription);
|
|
42
|
+
}
|
|
43
|
+
locationManagerDidChangeAuthorization(m: CLLocationManager): void {
|
|
44
|
+
const st = m.authorizationStatus;
|
|
45
|
+
if (st === CLAuthorizationStatus.kCLAuthorizationStatusAuthorizedWhenInUse ||
|
|
46
|
+
st === CLAuthorizationStatus.kCLAuthorizationStatusAuthorizedAlways) {
|
|
47
|
+
m.startUpdatingLocation();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// CNContactPickerViewController delegate. The picker resolution callbacks are set as instance fields
|
|
53
|
+
// after construction (replacing the old closure-captured `resolve`).
|
|
54
|
+
@NativeClass()
|
|
55
|
+
class ContactPickerDelegate extends NSObject implements CNContactPickerDelegate {
|
|
56
|
+
static ObjCProtocols = [CNContactPickerDelegate];
|
|
57
|
+
static new(): ContactPickerDelegate {
|
|
58
|
+
return <ContactPickerDelegate>super.new();
|
|
59
|
+
}
|
|
60
|
+
onSelect?: (contact: CNContact) => void;
|
|
61
|
+
onCancel?: (picker: CNContactPickerViewController) => void;
|
|
62
|
+
contactPickerDidSelectContact(_picker: CNContactPickerViewController, contact: CNContact): void {
|
|
63
|
+
this.onSelect?.(contact);
|
|
64
|
+
}
|
|
65
|
+
contactPickerDidCancel(picker: CNContactPickerViewController): void {
|
|
66
|
+
this.onCancel?.(picker);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// UIImagePickerController delegate (camera.capture). Result/options are set as instance fields after
|
|
71
|
+
// construction, replacing the old closure-captured `resolve`/`dataUrl`/`maxSize`.
|
|
72
|
+
@NativeClass()
|
|
73
|
+
class CameraCaptureDelegate extends NSObject implements UIImagePickerControllerDelegate, UINavigationControllerDelegate {
|
|
74
|
+
static ObjCProtocols = [UIImagePickerControllerDelegate, UINavigationControllerDelegate];
|
|
75
|
+
static new(): CameraCaptureDelegate {
|
|
76
|
+
return <CameraCaptureDelegate>super.new();
|
|
77
|
+
}
|
|
78
|
+
wantDataUrl = false;
|
|
79
|
+
maxSize = 1024;
|
|
80
|
+
onResult?: (out: { picked: boolean; width?: number; height?: number; dataUrl?: string }) => void;
|
|
81
|
+
imagePickerControllerDidFinishPickingMediaWithInfo(picker: UIImagePickerController, info: NSDictionary<string, any>): void {
|
|
82
|
+
picker.dismissViewControllerAnimatedCompletion(true, null);
|
|
83
|
+
const img = info.objectForKey(UIImagePickerControllerOriginalImage) as UIImage | null;
|
|
84
|
+
if (!img) return this.onResult?.({ picked: true });
|
|
85
|
+
const out: { picked: boolean; width?: number; height?: number; dataUrl?: string } =
|
|
86
|
+
{ picked: true, width: img.size.width, height: img.size.height };
|
|
87
|
+
if (this.wantDataUrl) out.dataUrl = uiImageToDataUrl(img, this.maxSize);
|
|
88
|
+
this.onResult?.(out);
|
|
89
|
+
}
|
|
90
|
+
imagePickerControllerDidCancel(picker: UIImagePickerController): void {
|
|
91
|
+
picker.dismissViewControllerAnimatedCompletion(true, null);
|
|
92
|
+
this.onResult?.({ picked: false });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
8
96
|
/**
|
|
9
97
|
* Extended-parity handlers: dialogs, storage.clear, geo watch, theme color,
|
|
10
98
|
* motion, contacts, calendar, camera. iOS-first like handlers-extended.
|
|
@@ -50,39 +138,15 @@ export function registerParityHandlers(): void {
|
|
|
50
138
|
});
|
|
51
139
|
|
|
52
140
|
// ── geo.watch (persistent CLLocationManager) ───────────────────────
|
|
53
|
-
let geoManager:
|
|
54
|
-
let geoDelegate:
|
|
141
|
+
let geoManager: CLLocationManager | null = null;
|
|
142
|
+
let geoDelegate: GeoWatchDelegate | null = null;
|
|
55
143
|
|
|
56
144
|
bridge.register('geo.watch.start', () => {
|
|
57
145
|
if (!isIOS) throw iosOnly();
|
|
58
146
|
if (geoManager) return; // already streaming
|
|
59
|
-
const DelegateClass = (NSObject as any).extend(
|
|
60
|
-
{
|
|
61
|
-
locationManagerDidUpdateLocations(_m: CLLocationManager, locations: NSArray<CLLocation>) {
|
|
62
|
-
const loc = locations.lastObject;
|
|
63
|
-
if (!loc) return;
|
|
64
|
-
bridge.emit('geo.position', {
|
|
65
|
-
lat: loc.coordinate.latitude,
|
|
66
|
-
lng: loc.coordinate.longitude,
|
|
67
|
-
accuracy: loc.horizontalAccuracy,
|
|
68
|
-
});
|
|
69
|
-
},
|
|
70
|
-
locationManagerDidFailWithError(_m: CLLocationManager, error: NSError) {
|
|
71
|
-
console.warn('AppWrap: geo.watch error', error.localizedDescription);
|
|
72
|
-
},
|
|
73
|
-
locationManagerDidChangeAuthorization(m: CLLocationManager) {
|
|
74
|
-
const st = m.authorizationStatus;
|
|
75
|
-
if (st === CLAuthorizationStatus.kCLAuthorizationStatusAuthorizedWhenInUse ||
|
|
76
|
-
st === CLAuthorizationStatus.kCLAuthorizationStatusAuthorizedAlways) {
|
|
77
|
-
m.startUpdatingLocation();
|
|
78
|
-
}
|
|
79
|
-
},
|
|
80
|
-
},
|
|
81
|
-
{ protocols: [CLLocationManagerDelegate] }
|
|
82
|
-
);
|
|
83
147
|
Utils.dispatchToMainThread(() => {
|
|
84
148
|
geoManager = CLLocationManager.new();
|
|
85
|
-
geoDelegate =
|
|
149
|
+
geoDelegate = GeoWatchDelegate.new();
|
|
86
150
|
geoManager.delegate = geoDelegate;
|
|
87
151
|
geoManager.desiredAccuracy = kCLLocationAccuracyHundredMeters;
|
|
88
152
|
const st = geoManager.authorizationStatus;
|
|
@@ -105,8 +169,8 @@ export function registerParityHandlers(): void {
|
|
|
105
169
|
// block: the repeated JS↔Obj-C block is fragile under NativeScript (the handler
|
|
106
170
|
// can silently stop firing on-device). startDeviceMotionUpdates() + reading
|
|
107
171
|
// `mm.deviceMotion` is a plain property read on the main thread — reliable.
|
|
108
|
-
let motionManager:
|
|
109
|
-
let motionTimer:
|
|
172
|
+
let motionManager: CMMotionManager | null = null;
|
|
173
|
+
let motionTimer: ReturnType<typeof setInterval> | null = null;
|
|
110
174
|
|
|
111
175
|
bridge.register('motion.start', () => {
|
|
112
176
|
if (!isIOS) throw iosOnly();
|
|
@@ -143,31 +207,16 @@ export function registerParityHandlers(): void {
|
|
|
143
207
|
if (!isIOS) throw iosOnly();
|
|
144
208
|
return new Promise((resolve) => {
|
|
145
209
|
Utils.dispatchToMainThread(() => {
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
resolve({
|
|
155
|
-
picked: true,
|
|
156
|
-
name: `${contact.givenName ?? ''} ${contact.familyName ?? ''}`.trim(),
|
|
157
|
-
phones: collect(contact.phoneNumbers, (v) => String(v.stringValue)),
|
|
158
|
-
emails: collect(contact.emailAddresses, (v) => String(v)),
|
|
159
|
-
});
|
|
160
|
-
},
|
|
161
|
-
contactPickerDidCancel(picker: any) {
|
|
162
|
-
picker.dismissViewControllerAnimatedCompletion(true, null);
|
|
163
|
-
resolve({ picked: false });
|
|
164
|
-
},
|
|
165
|
-
},
|
|
166
|
-
{ protocols: [CNContactPickerDelegate] }
|
|
167
|
-
);
|
|
210
|
+
const delegate = ContactPickerDelegate.new();
|
|
211
|
+
delegate.onSelect = (contact) => {
|
|
212
|
+
resolve({ picked: true, ...readContact(contact) });
|
|
213
|
+
};
|
|
214
|
+
delegate.onCancel = (picker) => {
|
|
215
|
+
picker.dismissViewControllerAnimatedCompletion(true, null);
|
|
216
|
+
resolve({ picked: false });
|
|
217
|
+
};
|
|
168
218
|
const picker = CNContactPickerViewController.new();
|
|
169
|
-
|
|
170
|
-
(picker as any)._appwrapDelegate = delegate; // retain
|
|
219
|
+
(picker as any)._appwrapDelegate = delegate; // retain delegate against ARC; not a typed property
|
|
171
220
|
picker.delegate = delegate;
|
|
172
221
|
Utils.ios.getRootViewController().presentViewControllerAnimatedCompletion(picker, true, null);
|
|
173
222
|
});
|
|
@@ -179,30 +228,23 @@ export function registerParityHandlers(): void {
|
|
|
179
228
|
if (!isIOS) throw iosOnly();
|
|
180
229
|
const store = CNContactStore.new();
|
|
181
230
|
return new Promise((resolve, reject) => {
|
|
182
|
-
store.requestAccessForEntityTypeCompletionHandler(CNEntityType.Contacts, (granted: boolean, error: NSError
|
|
231
|
+
store.requestAccessForEntityTypeCompletionHandler(CNEntityType.Contacts, (granted: boolean, error: NSError) => {
|
|
183
232
|
if (!granted) {
|
|
184
233
|
return reject(err('DENIED', error?.localizedDescription ?? 'contacts access denied'));
|
|
185
234
|
}
|
|
186
235
|
try {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
for (let i = 0; i < labeled.count; i++) out.push(map(labeled.objectAtIndex(i).value));
|
|
190
|
-
return out;
|
|
191
|
-
};
|
|
192
|
-
const keys = NSArray.arrayWithArray([
|
|
236
|
+
// Key string constants conform to CNKeyDescriptor (NSString) at runtime; type the array as such.
|
|
237
|
+
const keys = NSArray.arrayWithArray<CNKeyDescriptor>([
|
|
193
238
|
CNContactGivenNameKey,
|
|
194
239
|
CNContactFamilyNameKey,
|
|
195
240
|
CNContactPhoneNumbersKey,
|
|
196
241
|
CNContactEmailAddressesKey,
|
|
197
|
-
]
|
|
242
|
+
]);
|
|
198
243
|
const request = CNContactFetchRequest.alloc().initWithKeysToFetch(keys);
|
|
199
244
|
const contacts: Array<{ name: string; phones: string[]; emails: string[] }> = [];
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
phones: collect(contact.phoneNumbers, (v) => String(v.stringValue)),
|
|
204
|
-
emails: collect(contact.emailAddresses, (v) => String(v)),
|
|
205
|
-
});
|
|
245
|
+
// null error out-param (ObjC nil) — SDK types it as a non-nullable interop reference.
|
|
246
|
+
store.enumerateContactsWithFetchRequestErrorUsingBlock(request, null as any, (contact: CNContact) => {
|
|
247
|
+
contacts.push(readContact(contact));
|
|
206
248
|
});
|
|
207
249
|
resolve({ contacts });
|
|
208
250
|
} catch (e: any) {
|
|
@@ -228,8 +270,9 @@ export function registerParityHandlers(): void {
|
|
|
228
270
|
const startDate = start
|
|
229
271
|
? NSDate.dateWithTimeIntervalSince1970(Date.parse(start) / 1000)
|
|
230
272
|
: NSDate.dateWithTimeIntervalSinceNow(3600);
|
|
231
|
-
|
|
232
|
-
event.
|
|
273
|
+
// EKEvent.start/endDate are typed `Date`; NS marshals the NSDate we build here.
|
|
274
|
+
event.startDate = startDate as unknown as Date;
|
|
275
|
+
event.endDate = startDate.dateByAddingTimeInterval((durationMin ?? 60) * 60) as unknown as Date;
|
|
233
276
|
if (notes) event.notes = String(notes);
|
|
234
277
|
event.calendar = store.defaultCalendarForNewEvents;
|
|
235
278
|
store.saveEventSpanError(event, EKSpan.ThisEvent, null as any);
|
|
@@ -249,34 +292,20 @@ export function registerParityHandlers(): void {
|
|
|
249
292
|
});
|
|
250
293
|
|
|
251
294
|
// ── camera capture (UIImagePickerController) ───────────────────────
|
|
252
|
-
bridge.register('camera.capture', ({ dataUrl, maxSize }:
|
|
295
|
+
bridge.register('camera.capture', ({ dataUrl, maxSize }: { dataUrl?: boolean; maxSize?: number } = {}) => {
|
|
253
296
|
if (!isIOS) throw iosOnly();
|
|
254
297
|
if (!UIImagePickerController.isSourceTypeAvailable(UIImagePickerControllerSourceType.Camera)) {
|
|
255
298
|
throw err('UNSUPPORTED', 'No camera on this device (simulator?)');
|
|
256
299
|
}
|
|
257
300
|
return new Promise((resolve) => {
|
|
258
301
|
Utils.dispatchToMainThread(() => {
|
|
259
|
-
const DelegateClass = (NSObject as any).extend(
|
|
260
|
-
{
|
|
261
|
-
imagePickerControllerDidFinishPickingMediaWithInfo(picker: any, info: NSDictionary<string, any>) {
|
|
262
|
-
picker.dismissViewControllerAnimatedCompletion(true, null);
|
|
263
|
-
const img = info.objectForKey(UIImagePickerControllerOriginalImage) as UIImage | null;
|
|
264
|
-
if (!img) return resolve({ picked: true });
|
|
265
|
-
const out: any = { picked: true, width: img.size.width, height: img.size.height };
|
|
266
|
-
if (dataUrl) out.dataUrl = uiImageToDataUrl(img, maxSize ?? 1024);
|
|
267
|
-
resolve(out);
|
|
268
|
-
},
|
|
269
|
-
imagePickerControllerDidCancel(picker: any) {
|
|
270
|
-
picker.dismissViewControllerAnimatedCompletion(true, null);
|
|
271
|
-
resolve({ picked: false });
|
|
272
|
-
},
|
|
273
|
-
},
|
|
274
|
-
{ protocols: [UIImagePickerControllerDelegate, UINavigationControllerDelegate] }
|
|
275
|
-
);
|
|
276
302
|
const picker = UIImagePickerController.new();
|
|
277
303
|
picker.sourceType = UIImagePickerControllerSourceType.Camera;
|
|
278
|
-
const delegate =
|
|
279
|
-
|
|
304
|
+
const delegate = CameraCaptureDelegate.new();
|
|
305
|
+
delegate.wantDataUrl = !!dataUrl;
|
|
306
|
+
delegate.maxSize = maxSize ?? 1024;
|
|
307
|
+
delegate.onResult = resolve;
|
|
308
|
+
(picker as any)._appwrapDelegate = delegate; // interop: retain delegate past the present call (no typed slot)
|
|
280
309
|
picker.delegate = delegate;
|
|
281
310
|
Utils.ios.getRootViewController().presentViewControllerAnimatedCompletion(picker, true, null);
|
|
282
311
|
});
|
|
@@ -62,7 +62,7 @@ export function onApnsError(message: string): void {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
/** iOS AppDelegate / UN delegate → a remote notification arrived (tapped = user opened it). */
|
|
65
|
-
export function onRemoteMessage(userInfo: any, tapped: boolean): void {
|
|
65
|
+
export function onRemoteMessage(userInfo: any, tapped: boolean): void { // any: APNs userInfo is NSDictionary or auto-marshalled JS object (dual-path)
|
|
66
66
|
bridge.emit(tapped ? 'push.tap' : 'push.message', parseApnsPayload(userInfo));
|
|
67
67
|
}
|
|
68
68
|
|
|
@@ -74,6 +74,7 @@ export function onFcmMessage(data: Record<string, string>, title?: string, body?
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
/** APNs userInfo (NSDictionary or auto-marshalled object) → { data, title?, body? }. */
|
|
77
|
+
// any: dual-path payload (NSDictionary vs marshalled JS object) probed dynamically below.
|
|
77
78
|
function parseApnsPayload(userInfo: any): { data: Record<string, unknown>; title?: string; body?: string } {
|
|
78
79
|
const data: Record<string, unknown> = {};
|
|
79
80
|
let title: string | undefined;
|
|
@@ -166,18 +167,6 @@ export function registerPushHandlers(): void {
|
|
|
166
167
|
if (isAndroid) return androidDeleteToken();
|
|
167
168
|
});
|
|
168
169
|
|
|
169
|
-
// Dev/demo: ask the backend to push THIS device on demand. Goes native (NS Http) → no app:// CORS
|
|
170
|
-
// wall. Requires push.registrationUrl + a registered token; the backend honors `test:true`.
|
|
171
|
-
bridge.register('push.sendTest', () => {
|
|
172
|
-
const url = SHELL_CONFIG.pushRegistrationUrl;
|
|
173
|
-
if (!url) return Promise.reject(Object.assign(new Error('push.registrationUrl not configured'), { code: 'UNSUPPORTED' }));
|
|
174
|
-
if (!cachedToken) return Promise.reject(Object.assign(new Error('no device token yet — register first'), { code: 'NOT_READY' }));
|
|
175
|
-
return Http.request({
|
|
176
|
-
url, method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
177
|
-
content: JSON.stringify({ token: cachedToken, platform: isIOS ? 'ios' : 'android', test: true }),
|
|
178
|
-
}).then((res) => ({ status: res.statusCode }));
|
|
179
|
-
});
|
|
180
|
-
|
|
181
170
|
// Debug: auto-acquire + log the FCM token at boot so it's readable HEADLESSLY (adb logcat / appwrap
|
|
182
171
|
// logs) without a UI tap — the dev loop needs the token to send a test push. Android only (iOS push
|
|
183
172
|
// is off on personal teams); no-op when push isn't configured for this platform.
|
|
@@ -193,7 +182,7 @@ function unsupported(): Error {
|
|
|
193
182
|
}
|
|
194
183
|
|
|
195
184
|
// ── Android (Firebase Messaging) — guarded; classes exist only when FCM is wired ──
|
|
196
|
-
function fcm(): any {
|
|
185
|
+
function fcm(): any { // no NS types: third-party firebase (com.google.firebase.messaging)
|
|
197
186
|
const FM = (global as any).com?.google?.firebase?.messaging?.FirebaseMessaging;
|
|
198
187
|
if (!FM) throw Object.assign(new Error('FCM not configured — wire google-services.json + the messaging plugin'), { code: 'UNSUPPORTED' });
|
|
199
188
|
return FM.getInstance();
|
|
@@ -210,6 +199,7 @@ function androidRequestPermission(): Promise<string> {
|
|
|
210
199
|
const granted = ctx.checkSelfPermission('android.permission.POST_NOTIFICATIONS') === pm.PERMISSION_GRANTED;
|
|
211
200
|
if (granted) return resolve('granted');
|
|
212
201
|
const activity = Application.android.foregroundActivity || Application.android.startActivity;
|
|
202
|
+
// any: NS activity-result event payload (grantResults/requestCode) is untyped in core's event bus.
|
|
213
203
|
Application.android.on(Application.android.activityRequestPermissionsEvent as any, (args: any) => {
|
|
214
204
|
if (args.requestCode !== 7613) return;
|
|
215
205
|
const ok = args.grantResults?.length && args.grantResults[0] === pm.PERMISSION_GRANTED;
|
|
@@ -241,7 +231,7 @@ function androidGetToken(): Promise<PushToken> {
|
|
|
241
231
|
const OnCompleteListener = (global as any).com.google.android.gms.tasks.OnCompleteListener;
|
|
242
232
|
const task = fcm().getToken();
|
|
243
233
|
task.addOnCompleteListener(new OnCompleteListener({
|
|
244
|
-
onComplete(t: any) {
|
|
234
|
+
onComplete(t: any) { // no NS types: play-services gms Task<String> (FCM token)
|
|
245
235
|
if (t.isSuccessful()) {
|
|
246
236
|
const tok = String(t.getResult());
|
|
247
237
|
registerTokenWithBackend('android', tok); // parity with iOS onApnsToken
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Application, Utils, isAndroid, isIOS } from '@nativescript/core';
|
|
2
2
|
import { bridge } from './bridge';
|
|
3
3
|
|
|
4
|
-
// SKStoreReviewController
|
|
5
|
-
declare const com: any;
|
|
4
|
+
// SKStoreReviewController comes from the StoreKit typings referenced in references.d.ts.
|
|
5
|
+
declare const com: any; // no NS types: third-party play-core review (com.google.android.play.core.review)
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* In-app store review prompt — opt-in, strippable (its own handler file + group). Lives apart from
|
|
@@ -27,8 +27,8 @@ function registerIos(): void {
|
|
|
27
27
|
bridge.register('reviews.requestReview', () => {
|
|
28
28
|
Utils.dispatchToMainThread(() => {
|
|
29
29
|
const scene = UIApplication.sharedApplication.keyWindow?.windowScene;
|
|
30
|
-
if (scene && SKStoreReviewController.
|
|
31
|
-
SKStoreReviewController.
|
|
30
|
+
if (scene && SKStoreReviewController.requestReviewInScene) {
|
|
31
|
+
SKStoreReviewController.requestReviewInScene(scene);
|
|
32
32
|
} else {
|
|
33
33
|
SKStoreReviewController.requestReview();
|
|
34
34
|
}
|
|
@@ -49,7 +49,7 @@ function registerAndroid(): void {
|
|
|
49
49
|
// Both tasks COMPLETE even when the OS shows nothing (no Play track / rate-limited), so we
|
|
50
50
|
// resolve on completion regardless — never block, never fabricate a "shown" signal.
|
|
51
51
|
request.addOnCompleteListener(new com.google.android.gms.tasks.OnCompleteListener({
|
|
52
|
-
onComplete(infoTask: any) {
|
|
52
|
+
onComplete(infoTask: any) { // no NS types: play-services gms Task<ReviewInfo>
|
|
53
53
|
if (!infoTask.isSuccessful()) { resolve(); return; }
|
|
54
54
|
try {
|
|
55
55
|
const flow = manager.launchReviewFlow(activity, infoTask.getResult());
|
|
@@ -2,30 +2,36 @@ import { Utils, isAndroid, isIOS } from '@nativescript/core';
|
|
|
2
2
|
import { bridge } from './bridge';
|
|
3
3
|
import { requestPermissions, startActivityForResult } from './android-helpers';
|
|
4
4
|
|
|
5
|
-
// AVFoundation / UIKit globals (NS auto-links the frameworks via metadata when referenced).
|
|
6
|
-
declare const AVCaptureSession: any;
|
|
7
|
-
declare const AVCaptureDevice: any;
|
|
8
|
-
declare const AVCaptureDeviceInput: any;
|
|
9
|
-
declare const AVCaptureMetadataOutput: any;
|
|
10
|
-
declare const AVCaptureVideoPreviewLayer: any;
|
|
11
|
-
declare const AVMetadataObjectTypeQRCode: any;
|
|
12
|
-
declare const AVMetadataObjectTypeEAN13Code: any;
|
|
13
|
-
declare const AVMetadataObjectTypeCode128Code: any;
|
|
14
|
-
declare const AVLayerVideoGravityResizeAspectFill: any;
|
|
15
|
-
declare const AVCaptureDevicePositionFront: any;
|
|
16
|
-
declare const AVCaptureDevicePositionBack: any;
|
|
17
|
-
declare const AVCaptureDeviceDiscoverySession: any;
|
|
18
|
-
declare const AVMediaTypeVideo: any;
|
|
19
|
-
declare const AVCaptureDeviceTypeBuiltInWideAngleCamera: any;
|
|
20
|
-
declare const AVCaptureMetadataOutputObjectsDelegate: any;
|
|
21
5
|
// `dispatch_get_main_queue()` is a C macro, NOT a bridged symbol — NativeScript can't see it
|
|
22
6
|
// ("is not defined" at runtime). `dispatch_queue_create` IS a real bridged libdispatch function;
|
|
23
7
|
// a dedicated serial queue is the correct delegate queue for AVCaptureMetadataOutput anyway.
|
|
24
8
|
declare function dispatch_queue_create(label: string, attr: any): any;
|
|
25
|
-
declare const
|
|
9
|
+
declare const com: any; // no NS types: third-party ZXing (com.google.zxing.integration.android.*)
|
|
26
10
|
|
|
27
11
|
const err = (code: string, message: string) => Object.assign(new Error(message), { code });
|
|
28
12
|
|
|
13
|
+
// iOS metadata-output delegate. NativeScript uses the declared protocol to map the camelCase JS
|
|
14
|
+
// method to the colonated selector `captureOutput:didOutputMetadataObjects:fromConnection:`; with an
|
|
15
|
+
// empty protocols list it registers under the wrong selector and AVFoundation never calls it — the
|
|
16
|
+
// scanner opens but never decodes. The first decoded object is forwarded via the `onResult` instance
|
|
17
|
+
// field (assigned after `new()`), replacing the original closure capture.
|
|
18
|
+
@NativeClass()
|
|
19
|
+
class MetadataDelegate extends NSObject implements AVCaptureMetadataOutputObjectsDelegate {
|
|
20
|
+
static ObjCProtocols = [AVCaptureMetadataOutputObjectsDelegate];
|
|
21
|
+
onResult?: (obj: AVMetadataObject) => void;
|
|
22
|
+
static new(): MetadataDelegate {
|
|
23
|
+
return <MetadataDelegate>super.new();
|
|
24
|
+
}
|
|
25
|
+
captureOutputDidOutputMetadataObjectsFromConnection(
|
|
26
|
+
_output: AVCaptureOutput,
|
|
27
|
+
metadataObjects: NSArray<AVMetadataObject> | AVMetadataObject[],
|
|
28
|
+
_connection: AVCaptureConnection
|
|
29
|
+
): void {
|
|
30
|
+
const obj = (metadataObjects as NSArray<AVMetadataObject>)?.firstObject;
|
|
31
|
+
if (obj) this.onResult?.(obj);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
29
35
|
/**
|
|
30
36
|
* Camera barcode / QR decoding. iOS: AVCaptureMetadataOutput in a full-screen capture VC presented
|
|
31
37
|
* over the WebView; first machine-readable code → stop session, dismiss, resolve. Android: ZXing's
|
|
@@ -41,12 +47,12 @@ export function registerScannerHandlers(): void {
|
|
|
41
47
|
}
|
|
42
48
|
|
|
43
49
|
/** Map our small enum → the platform metadata-object types. Empty/`all` = the full supported set. */
|
|
44
|
-
function iosFormatTypes(formats: any):
|
|
50
|
+
function iosFormatTypes(formats: any): string[] {
|
|
45
51
|
const all = [AVMetadataObjectTypeQRCode, AVMetadataObjectTypeEAN13Code, AVMetadataObjectTypeCode128Code];
|
|
46
52
|
const want = Array.isArray(formats) ? formats : formats ? [formats] : [];
|
|
47
53
|
const sel = want.filter((f: string) => f && f !== 'all');
|
|
48
54
|
if (!sel.length) return all;
|
|
49
|
-
const map: Record<string,
|
|
55
|
+
const map: Record<string, string> = {
|
|
50
56
|
qr: AVMetadataObjectTypeQRCode,
|
|
51
57
|
ean13: AVMetadataObjectTypeEAN13Code,
|
|
52
58
|
code128: AVMetadataObjectTypeCode128Code,
|
|
@@ -54,7 +60,7 @@ function iosFormatTypes(formats: any): any[] {
|
|
|
54
60
|
return sel.map((f: string) => map[f]).filter(Boolean);
|
|
55
61
|
}
|
|
56
62
|
|
|
57
|
-
function iosFormatName(type:
|
|
63
|
+
function iosFormatName(type: string): string {
|
|
58
64
|
if (type === AVMetadataObjectTypeQRCode) return 'qr';
|
|
59
65
|
if (type === AVMetadataObjectTypeEAN13Code) return 'ean13';
|
|
60
66
|
if (type === AVMetadataObjectTypeCode128Code) return 'code128';
|
|
@@ -98,7 +104,7 @@ function registerIos(): void {
|
|
|
98
104
|
Utils.dispatchToMainThread(() => {
|
|
99
105
|
try {
|
|
100
106
|
const wantFront = params?.camera === 'front';
|
|
101
|
-
const position = wantFront ?
|
|
107
|
+
const position = wantFront ? AVCaptureDevicePosition.Front : AVCaptureDevicePosition.Back;
|
|
102
108
|
const discovery = AVCaptureDeviceDiscoverySession.discoverySessionWithDeviceTypesMediaTypePosition(
|
|
103
109
|
[AVCaptureDeviceTypeBuiltInWideAngleCamera], AVMediaTypeVideo, position
|
|
104
110
|
);
|
|
@@ -118,25 +124,17 @@ function registerIos(): void {
|
|
|
118
124
|
|
|
119
125
|
// Delegate fires on a dedicated serial queue (see dispatch_queue_create below); settle()
|
|
120
126
|
// hops back to the main thread for the WKWebView bridge reply.
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
},
|
|
130
|
-
},
|
|
131
|
-
// The protocol MUST be declared: NativeScript uses it to map the camelCase JS method to the
|
|
132
|
-
// colonated selector `captureOutput:didOutputMetadataObjects:fromConnection:`. With an empty
|
|
133
|
-
// protocols list the method registers under the wrong selector and AVFoundation never calls
|
|
134
|
-
// it — the scanner opens but never decodes.
|
|
135
|
-
{ protocols: [AVCaptureMetadataOutputObjectsDelegate] }
|
|
136
|
-
);
|
|
137
|
-
activeDelegate = DelegateClass.new();
|
|
127
|
+
const delegate = MetadataDelegate.new();
|
|
128
|
+
delegate.onResult = (obj) => {
|
|
129
|
+
if (!pending) return;
|
|
130
|
+
const value = (obj as AVMetadataMachineReadableCodeObject).stringValue;
|
|
131
|
+
if (value == null) return;
|
|
132
|
+
settle({ value: String(value), format: iosFormatName(obj.type) });
|
|
133
|
+
};
|
|
134
|
+
activeDelegate = delegate;
|
|
138
135
|
output.setMetadataObjectsDelegateQueue(activeDelegate, dispatch_queue_create('cc.livx.scanner.metadata', null));
|
|
139
136
|
// metadataObjectTypes must be set AFTER addOutput (the available set is session-derived).
|
|
137
|
+
// interop: JS string[] is accepted at runtime where NSArray<string> is declared.
|
|
140
138
|
output.metadataObjectTypes = iosFormatTypes(params?.formats) as any;
|
|
141
139
|
|
|
142
140
|
const vc = UIViewController.new();
|