@livx.cc/appwrap 0.28.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/app.ts +55 -54
- package/runtime/app/shell/android-helpers.ts +6 -6
- package/runtime/app/shell/banner.ts +9 -4
- package/runtime/app/shell/bridge.ts +2 -0
- 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 +5 -4
- package/runtime/app/shell/handlers-android.ts +16 -12
- package/runtime/app/shell/handlers-background.ts +47 -36
- 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 -3
- package/runtime/app/shell/handlers-reviews.ts +5 -6
- 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 +1 -1
- package/runtime/app/shell/handlers.ts +4 -3
- package/runtime/app/shell/ios-frameworks.d.ts +9 -0
- 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 +6 -0
- package/runtime/tsconfig.json +1 -0
- package/src/cli.ts +43 -18
- package/src/derive.ts +8 -0
|
@@ -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 });
|
|
@@ -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;
|