craft-native 0.0.237 → 0.0.238
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/README.md +71 -0
- package/dist/android/src/emulator.d.ts +23 -0
- package/dist/android/src/index.d.ts +7 -1
- package/dist/android/src/index.js +280 -98
- package/dist/android/templates/CraftBridge.kt.template +65 -54
- package/dist/android/templates/CraftNative.kt.template +27 -0
- package/dist/android/templates/CraftNativeCapabilities.kt.template +36 -5
- package/dist/android/templates/CraftNativeServices.kt.template +45 -35
- package/dist/android/templates/CraftNativeShell.kt.template +103 -20
- package/dist/android/templates/LocationRecordingService.kt.template +252 -46
- package/dist/android-adb.d.ts +2 -0
- package/dist/api/index.d.ts +1 -1
- package/dist/api/mobile.d.ts +49 -14
- package/dist/browser/storage.d.ts +3 -0
- package/dist/browser.js +163 -51
- package/dist/cli.js +1 -1
- package/dist/index.cjs +126 -12
- package/dist/index.js +126 -12
- package/dist/ios/src/index.d.ts +5 -0
- package/dist/ios/src/index.js +9 -1
- package/dist/ios/src/testing.d.ts +2 -0
- package/dist/ios/templates/CraftApp.swift +376 -88
- package/dist/ios/templates/CraftHeartRate.swift +141 -44
- package/dist/ios/templates/CraftNativeActions.swift +46 -48
- package/dist/ios/templates/CraftNativeScreen.swift +11 -4
- package/dist/ios/templates/CraftPersistentStorage.swift +99 -0
- package/dist/ios/templates/CraftRecordingArchive.swift +85 -0
- package/dist/ios/templates/testing/DriverTests.swift.template +8 -1
- package/dist/mobile-testing.js +30 -4
- package/dist/mobile.js +169 -17
- package/package.json +1 -1
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import CryptoKit
|
|
2
|
+
import Foundation
|
|
3
|
+
import Darwin
|
|
4
|
+
|
|
5
|
+
/// Immutable completed recordings and durable acknowledgement tombstones.
|
|
6
|
+
/// IDs are opaque values; their SHA-256, never caller path text, names a file.
|
|
7
|
+
enum CraftRecordingArchive {
|
|
8
|
+
typealias Writer = (Data, URL) throws -> Void
|
|
9
|
+
static func writeAtomically(_ data: Data, _ url: URL) throws {
|
|
10
|
+
try FileManager.default.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true)
|
|
11
|
+
let temporary = url.deletingLastPathComponent().appendingPathComponent(".craft-atomic-" + UUID().uuidString)
|
|
12
|
+
defer { try? FileManager.default.removeItem(at: temporary) }
|
|
13
|
+
try data.write(to: temporary, options: .withoutOverwriting)
|
|
14
|
+
let file = try FileHandle(forWritingTo: temporary)
|
|
15
|
+
do { try file.synchronize(); try file.close() }
|
|
16
|
+
catch { try? file.close(); throw error }
|
|
17
|
+
try FileManager.default.setAttributes([.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication], ofItemAtPath: temporary.path)
|
|
18
|
+
var protected = temporary
|
|
19
|
+
var values = URLResourceValues()
|
|
20
|
+
values.isExcludedFromBackup = true
|
|
21
|
+
try protected.setResourceValues(values)
|
|
22
|
+
let result = temporary.path.withCString { source in url.path.withCString { destination in Darwin.rename(source, destination) } }
|
|
23
|
+
if result != 0 { throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno)) }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private static func url(_ directory: URL, _ kind: String, _ id: String, acknowledged: Bool = false) -> URL {
|
|
27
|
+
let digest = SHA256.hash(data: Data(id.utf8)).map { String(format: "%02x", $0) }.joined()
|
|
28
|
+
return directory.appendingPathComponent("craft-\(kind)-\(acknowledged ? "ack" : "completed")-\(digest).json")
|
|
29
|
+
}
|
|
30
|
+
static func startId(_ options: [String: Any], directory: URL, kind: String, currentId: String?) throws -> String {
|
|
31
|
+
let id: String
|
|
32
|
+
if options.keys.contains("recordingId") {
|
|
33
|
+
guard let requested = options["recordingId"] as? String,
|
|
34
|
+
!requested.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
|
35
|
+
throw CraftNativeActionError(code: "INVALID_ARGUMENT", message: "recordingId must be a nonempty recording ID")
|
|
36
|
+
}
|
|
37
|
+
id = requested
|
|
38
|
+
} else { id = UUID().uuidString }
|
|
39
|
+
let acknowledged = try isAcknowledged(directory, kind, id)
|
|
40
|
+
if id == currentId || acknowledged || FileManager.default.fileExists(atPath: url(directory, kind, id).path) {
|
|
41
|
+
throw CraftNativeActionError(code: "RECORDING_CONFLICT", message: "Recording IDs cannot be reused")
|
|
42
|
+
}
|
|
43
|
+
return id
|
|
44
|
+
}
|
|
45
|
+
static func isAcknowledged(_ directory: URL, _ kind: String, _ id: String) throws -> Bool {
|
|
46
|
+
let marker = url(directory, kind, id, acknowledged: true)
|
|
47
|
+
guard FileManager.default.fileExists(atPath: marker.path) else { return false }
|
|
48
|
+
guard try Data(contentsOf: marker) == Data(id.utf8) else {
|
|
49
|
+
throw CraftNativeActionError(code: "RECORDING_STORAGE_ERROR", message: "Invalid recording acknowledgement")
|
|
50
|
+
}
|
|
51
|
+
return true
|
|
52
|
+
}
|
|
53
|
+
static func read(_ directory: URL, _ kind: String, _ id: String) throws -> [String: Any]? {
|
|
54
|
+
if try isAcknowledged(directory, kind, id) {
|
|
55
|
+
try acknowledge(directory, kind, id)
|
|
56
|
+
throw CraftNativeActionError(code: "RECORDING_NOT_FOUND", message: "This recording has been acknowledged")
|
|
57
|
+
}
|
|
58
|
+
let source = url(directory, kind, id)
|
|
59
|
+
guard FileManager.default.fileExists(atPath: source.path) else { return nil }
|
|
60
|
+
let data = try Data(contentsOf: source)
|
|
61
|
+
guard let value = try JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
62
|
+
value["id"] as? String == id, value["active"] as? Bool == false,
|
|
63
|
+
value[kind == "location" ? "locations" : "samples"] is [[String: Any]] else {
|
|
64
|
+
throw CraftNativeActionError(code: "RECORDING_STORAGE_ERROR", message: "Invalid completed recording archive")
|
|
65
|
+
}
|
|
66
|
+
return value
|
|
67
|
+
}
|
|
68
|
+
static func save(_ directory: URL, _ kind: String, _ state: [String: Any], samples: [[String: Any]], writer: Writer = writeAtomically) throws {
|
|
69
|
+
guard let id = state["id"] as? String else { return }
|
|
70
|
+
if try isAcknowledged(directory, kind, id) { try acknowledge(directory, kind, id); return }
|
|
71
|
+
if try read(directory, kind, id) != nil { return }
|
|
72
|
+
var completed = state
|
|
73
|
+
completed["active"] = false
|
|
74
|
+
completed["paused"] = false
|
|
75
|
+
completed["sampleCount"] = samples.count
|
|
76
|
+
completed[kind == "location" ? "locations" : "samples"] = samples
|
|
77
|
+
try writer(JSONSerialization.data(withJSONObject: completed), url(directory, kind, id))
|
|
78
|
+
}
|
|
79
|
+
static func acknowledge(_ directory: URL, _ kind: String, _ id: String) throws {
|
|
80
|
+
let marker = url(directory, kind, id, acknowledged: true)
|
|
81
|
+
if try !isAcknowledged(directory, kind, id) { try writeAtomically(Data(id.utf8), marker) }
|
|
82
|
+
let archive = url(directory, kind, id)
|
|
83
|
+
if FileManager.default.fileExists(atPath: archive.path) { try FileManager.default.removeItem(at: archive) }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -53,6 +53,7 @@ final class DriverTests: XCTestCase {
|
|
|
53
53
|
let control = ProcessInfo.processInfo.environment["CRAFT_TEST_CONTROL"] ?? {{CONTROL_URL}}
|
|
54
54
|
let app = XCUIApplication(bundleIdentifier: {{BUNDLE_ID}})
|
|
55
55
|
let tabs: [String] = {{TABS}}
|
|
56
|
+
let tabIdentifiers: [String: String] = {{TAB_IDENTIFIERS}}
|
|
56
57
|
private var bundledJourney = false
|
|
57
58
|
|
|
58
59
|
func get(_ path: String, timeout: Double = 15) -> Data? {
|
|
@@ -140,9 +141,15 @@ final class DriverTests: XCTestCase {
|
|
|
140
141
|
|
|
141
142
|
/// The native tab bar's button: Craft's floating bar is not a UITabBar, and
|
|
142
143
|
/// XCUITest calls its buttons not hittable, so it is the button with this
|
|
143
|
-
/// label in the bottom fifth of the window.
|
|
144
|
+
/// label in the bottom fifth of the window. A configured stable identifier
|
|
145
|
+
/// keeps dynamic captions from changing the tab's native identity.
|
|
144
146
|
func tabButton(_ name: String) -> XCUIElement? {
|
|
145
147
|
let height = app.windows.firstMatch.frame.height
|
|
148
|
+
if let identifier = tabIdentifiers[name] {
|
|
149
|
+
let candidate = app.buttons.matching(NSPredicate(format: "identifier == %@", identifier)).firstMatch
|
|
150
|
+
if candidate.exists && candidate.frame.minY > height * 0.8 && candidate.frame.maxY <= height + 1 && candidate.frame.width > 0 { return candidate }
|
|
151
|
+
return nil
|
|
152
|
+
}
|
|
146
153
|
let matches = app.buttons.matching(NSPredicate(format: "label == %@", name))
|
|
147
154
|
for index in 0..<min(matches.count, 10) {
|
|
148
155
|
let candidate = matches.element(boundBy: index)
|
package/dist/mobile-testing.js
CHANGED
|
@@ -8,6 +8,35 @@ import { existsSync } from "fs";
|
|
|
8
8
|
import { mkdir, mkdtemp, rm } from "fs/promises";
|
|
9
9
|
import { homedir, tmpdir } from "os";
|
|
10
10
|
import { isAbsolute, join } from "path";
|
|
11
|
+
|
|
12
|
+
// src/android-adb.ts
|
|
13
|
+
async function requireAndroidEmulatorRoot(adb, serial, timeoutMs = 60000) {
|
|
14
|
+
if (!/^emulator-\d+$/.test(serial))
|
|
15
|
+
throw new Error("An explicit disposable Android emulator serial is required");
|
|
16
|
+
const deadline = Date.now() + timeoutMs;
|
|
17
|
+
async function run(args) {
|
|
18
|
+
const remaining = deadline - Date.now();
|
|
19
|
+
if (remaining <= 0)
|
|
20
|
+
throw new Error("Android root reconnect deadline exceeded");
|
|
21
|
+
const child = Bun.spawn([adb, "-s", serial, ...args], { stdout: "pipe", stderr: "pipe", timeout: remaining });
|
|
22
|
+
const [code, stdout, stderr] = await Promise.all([child.exited, new Response(child.stdout).text(), new Response(child.stderr).text()]);
|
|
23
|
+
return { code, output: `${stdout}
|
|
24
|
+
${stderr}`.trim() };
|
|
25
|
+
}
|
|
26
|
+
const root = await run(["root"]);
|
|
27
|
+
if (root.code !== 0 && !/^adb: unable to connect for root: closed$/.test(root.output)) {
|
|
28
|
+
throw new Error(`adb root failed (${root.code}): ${root.output}`);
|
|
29
|
+
}
|
|
30
|
+
const connected = await run(["wait-for-device"]);
|
|
31
|
+
if (connected.code !== 0)
|
|
32
|
+
throw new Error(`Android root reconnect failed (${connected.code}): ${connected.output}`);
|
|
33
|
+
const identity = await run(["shell", "id", "-u"]);
|
|
34
|
+
if (identity.code !== 0 || identity.output !== "0") {
|
|
35
|
+
throw new Error(`Storage fault tests require a rootable disposable AOSP emulator: ${identity.output}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/mobile-media-testing.ts
|
|
11
40
|
async function verifyMobileMediaCache(options) {
|
|
12
41
|
if (options.platform !== "ios" && options.platform !== "android")
|
|
13
42
|
throw new Error("Choose ios or android");
|
|
@@ -207,10 +236,7 @@ ${await Bun.file(join(fixtures, "media-cache.kt")).text()}`);
|
|
|
207
236
|
throw new Error(result);
|
|
208
237
|
print(result);
|
|
209
238
|
};
|
|
210
|
-
await
|
|
211
|
-
await run([adb, "-s", serial, "wait-for-device"]);
|
|
212
|
-
if ((await run([adb, "-s", serial, "shell", "id", "-u"])).trim() !== "0")
|
|
213
|
-
throw new Error("Storage fault tests require a rootable disposable AOSP emulator");
|
|
239
|
+
await requireAndroidEmulatorRoot(adb, serial);
|
|
214
240
|
await run([adb, "-s", serial, "shell", "run-as", packageName, "mkdir", "-p", "files/craft-media"]);
|
|
215
241
|
const cache = `/data/user/0/${packageName}/files/craft-media`;
|
|
216
242
|
const owner = (await run([adb, "-s", serial, "shell", "stat", "-c", "%u:%g", cache])).trim();
|
package/dist/mobile.js
CHANGED
|
@@ -355,6 +355,107 @@ var secureStorage = {
|
|
|
355
355
|
Object.keys(localStorage).filter((k) => k.startsWith("secure_")).forEach((k) => localStorage.removeItem(k));
|
|
356
356
|
}
|
|
357
357
|
};
|
|
358
|
+
function storageHost(method) {
|
|
359
|
+
const host = getCraftRoot()?.storage;
|
|
360
|
+
if (typeof host?.[method] !== "function")
|
|
361
|
+
throw Object.assign(new Error("This host does not provide native storage"), { code: "STORAGE_UNSUPPORTED" });
|
|
362
|
+
return host;
|
|
363
|
+
}
|
|
364
|
+
var crypto = {
|
|
365
|
+
async randomUUID() {
|
|
366
|
+
const root = getCraftRoot();
|
|
367
|
+
if (!root)
|
|
368
|
+
return secureUUID();
|
|
369
|
+
const caps = root.capabilities;
|
|
370
|
+
const advertised = Array.isArray(caps) ? caps.includes("crypto") : caps?.crypto === true;
|
|
371
|
+
if (!advertised || typeof root.crypto?.randomUUID !== "function")
|
|
372
|
+
throw Object.assign(new Error("This host does not provide secure UUIDs"), { code: "CRYPTO_UNSUPPORTED" });
|
|
373
|
+
const id = await root.crypto.randomUUID();
|
|
374
|
+
if (typeof id !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(id))
|
|
375
|
+
throw Object.assign(new Error("The host returned an invalid secure UUID"), { code: "CRYPTO_INVALID_RESPONSE" });
|
|
376
|
+
return id;
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
var storage = {
|
|
380
|
+
supportsSetIfAbsent() {
|
|
381
|
+
const root = getCraftRoot();
|
|
382
|
+
const caps = root?.capabilities;
|
|
383
|
+
const supported = Array.isArray(caps) ? caps.includes("storageSetIfAbsent") : caps?.storageSetIfAbsent === true;
|
|
384
|
+
return supported && typeof root?.storage?.setIfAbsent === "function";
|
|
385
|
+
},
|
|
386
|
+
supportsCompareAndSet() {
|
|
387
|
+
const root = getCraftRoot();
|
|
388
|
+
const caps = root?.capabilities;
|
|
389
|
+
const supported = Array.isArray(caps) ? caps.includes("storageCompareAndSet") : caps?.storageCompareAndSet === true;
|
|
390
|
+
return supported && typeof root?.storage?.compareAndSet === "function";
|
|
391
|
+
},
|
|
392
|
+
async get(key) {
|
|
393
|
+
return storageHost("get").get(key);
|
|
394
|
+
},
|
|
395
|
+
async set(key, value) {
|
|
396
|
+
return storageHost("set").set(key, value);
|
|
397
|
+
},
|
|
398
|
+
async remove(key) {
|
|
399
|
+
return storageHost("remove").remove(key);
|
|
400
|
+
},
|
|
401
|
+
async clear() {
|
|
402
|
+
return storageHost("clear").clear();
|
|
403
|
+
},
|
|
404
|
+
async keys() {
|
|
405
|
+
return storageHost("keys").keys();
|
|
406
|
+
},
|
|
407
|
+
async setIfAbsent(key, value) {
|
|
408
|
+
if (!storage.supportsSetIfAbsent())
|
|
409
|
+
throw Object.assign(new Error("This app build cannot atomically register an intent"), { code: "STORAGE_SET_IF_ABSENT_UNSUPPORTED" });
|
|
410
|
+
if (typeof key !== "string" || !key.trim() || JSON.stringify(value) === undefined)
|
|
411
|
+
throw Object.assign(new Error("setIfAbsent needs a key and JSON value"), { code: "INVALID_ARGUMENT" });
|
|
412
|
+
return storageHost("setIfAbsent").setIfAbsent(key, value);
|
|
413
|
+
},
|
|
414
|
+
async compareAndSet(key, expectedValue, nextValue) {
|
|
415
|
+
if (!storage.supportsCompareAndSet())
|
|
416
|
+
throw Object.assign(new Error("This app build cannot atomically checkpoint existing storage"), { code: "STORAGE_COMPARE_AND_SET_UNSUPPORTED" });
|
|
417
|
+
if (typeof key !== "string" || !key.trim() || JSON.stringify(expectedValue) === undefined || JSON.stringify(nextValue) === undefined)
|
|
418
|
+
throw Object.assign(new Error("compareAndSet needs a key and JSON values"), { code: "INVALID_ARGUMENT" });
|
|
419
|
+
return storageHost("compareAndSet").compareAndSet(key, expectedValue, nextValue);
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
function recordingOwnershipSupported(namespace) {
|
|
423
|
+
const root = getCraftRoot();
|
|
424
|
+
const flag = namespace === "location" ? "locationRecordingOwnership" : "heartRateRecordingOwnership";
|
|
425
|
+
const caps = root?.capabilities;
|
|
426
|
+
const advertised = Array.isArray(caps) ? caps.includes(flag) : caps?.[flag] === true;
|
|
427
|
+
return advertised && ["pauseRecording", "resumeRecording", "stopRecording", "readRecording", "getRecordingState", "acknowledgeRecording"].every((method) => typeof root?.[namespace]?.[method] === "function");
|
|
428
|
+
}
|
|
429
|
+
function recordingIntentSupported(namespace) {
|
|
430
|
+
const root = getCraftRoot();
|
|
431
|
+
const flag = namespace === "location" ? "locationRecordingIntentIdentity" : "heartRateRecordingIntentIdentity";
|
|
432
|
+
const caps = root?.capabilities;
|
|
433
|
+
const advertised = Array.isArray(caps) ? caps.includes(flag) : caps?.[flag] === true;
|
|
434
|
+
return advertised && recordingOwnershipSupported(namespace) && typeof root?.[namespace]?.startRecording === "function";
|
|
435
|
+
}
|
|
436
|
+
function validateRecordingStart(namespace, options) {
|
|
437
|
+
if (!Object.prototype.hasOwnProperty.call(options, "recordingId"))
|
|
438
|
+
return;
|
|
439
|
+
if (typeof options.recordingId !== "string" || !options.recordingId.trim())
|
|
440
|
+
throw Object.assign(new Error("recordingId must be a nonempty recording ID"), { code: "INVALID_ARGUMENT" });
|
|
441
|
+
if (!recordingIntentSupported(namespace))
|
|
442
|
+
throw Object.assign(new Error("This app build cannot start the persisted recording intent"), { code: "RECORDING_INTENT_IDENTITY_UNSUPPORTED" });
|
|
443
|
+
}
|
|
444
|
+
function verifyRecordingStart(options, result) {
|
|
445
|
+
if (options.recordingId !== undefined && result.id !== options.recordingId)
|
|
446
|
+
throw Object.assign(new Error("The native recording does not match the persisted intent"), { code: "RECORDING_CONFLICT" });
|
|
447
|
+
return result;
|
|
448
|
+
}
|
|
449
|
+
function validateRecordingOwnership(namespace, options) {
|
|
450
|
+
if (!options || !Object.prototype.hasOwnProperty.call(options, "expectedId"))
|
|
451
|
+
return;
|
|
452
|
+
if (typeof options.expectedId !== "string" || !options.expectedId.trim())
|
|
453
|
+
throw Object.assign(new Error("expectedId must be a nonempty recording ID"), { code: "INVALID_ARGUMENT" });
|
|
454
|
+
if ((getCraftRoot() || namespace === "heartRate") && !recordingOwnershipSupported(namespace))
|
|
455
|
+
throw Object.assign(new Error("This app build cannot enforce recording ownership"), { code: "RECORDING_OWNERSHIP_UNSUPPORTED" });
|
|
456
|
+
if (namespace === "location" && !getCraftRoot() && options.expectedId !== webLocationRecording.id)
|
|
457
|
+
throw Object.assign(new Error("The active location recording has changed"), { code: "RECORDING_CONFLICT" });
|
|
458
|
+
}
|
|
358
459
|
var webLocationRecording = {
|
|
359
460
|
id: null,
|
|
360
461
|
active: false,
|
|
@@ -364,6 +465,8 @@ var webLocationRecording = {
|
|
|
364
465
|
};
|
|
365
466
|
var webLocationRecordingWatchId = null;
|
|
366
467
|
var location = {
|
|
468
|
+
supportsRecordingOwnership: () => recordingOwnershipSupported("location"),
|
|
469
|
+
supportsRecordingIntentIdentity: () => recordingIntentSupported("location"),
|
|
367
470
|
async getCurrentPosition(options = {}) {
|
|
368
471
|
const craft = getCraftMobile();
|
|
369
472
|
if (typeof window !== "undefined" && craft?.location) {
|
|
@@ -412,9 +515,12 @@ var location = {
|
|
|
412
515
|
navigator.geolocation.clearWatch(watchId);
|
|
413
516
|
},
|
|
414
517
|
async startRecording(options = {}) {
|
|
518
|
+
validateRecordingStart("location", options);
|
|
415
519
|
const craft = getCraftMobile();
|
|
416
520
|
if (typeof window !== "undefined" && craft?.location?.startRecording)
|
|
417
|
-
return craft.location.startRecording(options);
|
|
521
|
+
return verifyRecordingStart(options, await craft.location.startRecording(options));
|
|
522
|
+
if (webLocationRecording.active)
|
|
523
|
+
throw Object.assign(new Error("A location recording is already active"), { code: "RECORDING_ALREADY_ACTIVE" });
|
|
418
524
|
if (webLocationRecordingWatchId !== null)
|
|
419
525
|
navigator.geolocation.clearWatch(webLocationRecordingWatchId);
|
|
420
526
|
webLocationRecording = {
|
|
@@ -430,24 +536,27 @@ var location = {
|
|
|
430
536
|
}, options);
|
|
431
537
|
return { ...webLocationRecording, sampleCount: 0 };
|
|
432
538
|
},
|
|
433
|
-
async pauseRecording() {
|
|
539
|
+
async pauseRecording(options) {
|
|
540
|
+
validateRecordingOwnership("location", options);
|
|
434
541
|
const craft = getCraftMobile();
|
|
435
542
|
if (typeof window !== "undefined" && craft?.location?.pauseRecording)
|
|
436
|
-
return craft.location.pauseRecording();
|
|
543
|
+
return craft.location.pauseRecording(options);
|
|
437
544
|
webLocationRecording.paused = true;
|
|
438
545
|
return { ...webLocationRecording, sampleCount: webLocationRecording.locations.length };
|
|
439
546
|
},
|
|
440
|
-
async resumeRecording() {
|
|
547
|
+
async resumeRecording(options) {
|
|
548
|
+
validateRecordingOwnership("location", options);
|
|
441
549
|
const craft = getCraftMobile();
|
|
442
550
|
if (typeof window !== "undefined" && craft?.location?.resumeRecording)
|
|
443
|
-
return craft.location.resumeRecording();
|
|
551
|
+
return craft.location.resumeRecording(options);
|
|
444
552
|
webLocationRecording.paused = false;
|
|
445
553
|
return { ...webLocationRecording, sampleCount: webLocationRecording.locations.length };
|
|
446
554
|
},
|
|
447
|
-
async stopRecording() {
|
|
555
|
+
async stopRecording(options) {
|
|
556
|
+
validateRecordingOwnership("location", options);
|
|
448
557
|
const craft = getCraftMobile();
|
|
449
558
|
if (typeof window !== "undefined" && craft?.location?.stopRecording)
|
|
450
|
-
return craft.location.stopRecording();
|
|
559
|
+
return craft.location.stopRecording(options);
|
|
451
560
|
if (webLocationRecordingWatchId !== null) {
|
|
452
561
|
location.clearWatch(webLocationRecordingWatchId);
|
|
453
562
|
webLocationRecordingWatchId = null;
|
|
@@ -456,16 +565,30 @@ var location = {
|
|
|
456
565
|
webLocationRecording.paused = false;
|
|
457
566
|
return { ...webLocationRecording, locations: [...webLocationRecording.locations] };
|
|
458
567
|
},
|
|
459
|
-
async getRecordingState() {
|
|
568
|
+
async getRecordingState(options) {
|
|
569
|
+
validateRecordingOwnership("location", options);
|
|
460
570
|
const craft = getCraftMobile();
|
|
461
571
|
if (typeof window !== "undefined" && craft?.location?.getRecordingState)
|
|
462
|
-
return craft.location.getRecordingState();
|
|
572
|
+
return craft.location.getRecordingState(options);
|
|
463
573
|
return { ...webLocationRecording, sampleCount: webLocationRecording.locations.length };
|
|
464
574
|
},
|
|
465
|
-
async
|
|
575
|
+
async acknowledgeRecording(options) {
|
|
576
|
+
if (!options || !Object.prototype.hasOwnProperty.call(options, "expectedId"))
|
|
577
|
+
throw Object.assign(new Error("Acknowledgement needs expectedId"), { code: "INVALID_ARGUMENT" });
|
|
578
|
+
validateRecordingOwnership("location", options);
|
|
579
|
+
const craft = getCraftMobile();
|
|
580
|
+
if (craft?.location?.acknowledgeRecording)
|
|
581
|
+
return craft.location.acknowledgeRecording(options);
|
|
582
|
+
if (webLocationRecording.active)
|
|
583
|
+
throw Object.assign(new Error("The recording is still active"), { code: "RECORDING_ALREADY_ACTIVE" });
|
|
584
|
+
webLocationRecording = { id: null, active: false, paused: false, startedAt: null, locations: [] };
|
|
585
|
+
return true;
|
|
586
|
+
},
|
|
587
|
+
async readRecording(options) {
|
|
588
|
+
validateRecordingOwnership("location", options);
|
|
466
589
|
const craft = getCraftMobile();
|
|
467
590
|
if (typeof window !== "undefined" && craft?.location?.readRecording)
|
|
468
|
-
return craft.location.readRecording();
|
|
591
|
+
return craft.location.readRecording(options);
|
|
469
592
|
return [...webLocationRecording.locations];
|
|
470
593
|
}
|
|
471
594
|
};
|
|
@@ -1023,6 +1146,8 @@ async function heartRateCall(method, fallback, normalize, ...args) {
|
|
|
1023
1146
|
}
|
|
1024
1147
|
var noRecording = () => normalizeHeartRateRecording({});
|
|
1025
1148
|
var heartRate = {
|
|
1149
|
+
supportsRecordingOwnership: () => recordingOwnershipSupported("heartRate"),
|
|
1150
|
+
supportsRecordingIntentIdentity: () => recordingIntentSupported("heartRate"),
|
|
1026
1151
|
isAvailable() {
|
|
1027
1152
|
return typeof nativeHeartRate()?.getState === "function";
|
|
1028
1153
|
},
|
|
@@ -1037,12 +1162,37 @@ var heartRate = {
|
|
|
1037
1162
|
connect: (id) => heartRateCall("connect", noHeartRate, normalizeHeartRateState, id),
|
|
1038
1163
|
reconnect: () => heartRateCall("reconnect", noHeartRate, normalizeHeartRateState),
|
|
1039
1164
|
disconnect: (options = {}) => heartRateCall("disconnect", noHeartRate, normalizeHeartRateState, options),
|
|
1040
|
-
startRecording
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1165
|
+
async startRecording(options) {
|
|
1166
|
+
const supplied = options ?? {};
|
|
1167
|
+
validateRecordingStart("heartRate", supplied);
|
|
1168
|
+
return verifyRecordingStart(supplied, await heartRateCall("startRecording", noRecording, normalizeHeartRateRecording, ...options === undefined ? [] : [options]));
|
|
1169
|
+
},
|
|
1170
|
+
async pauseRecording(options) {
|
|
1171
|
+
validateRecordingOwnership("heartRate", options);
|
|
1172
|
+
return heartRateCall("pauseRecording", noRecording, normalizeHeartRateRecording, ...options === undefined ? [] : [options]);
|
|
1173
|
+
},
|
|
1174
|
+
async resumeRecording(options) {
|
|
1175
|
+
validateRecordingOwnership("heartRate", options);
|
|
1176
|
+
return heartRateCall("resumeRecording", noRecording, normalizeHeartRateRecording, ...options === undefined ? [] : [options]);
|
|
1177
|
+
},
|
|
1178
|
+
async stopRecording(options) {
|
|
1179
|
+
validateRecordingOwnership("heartRate", options);
|
|
1180
|
+
return heartRateCall("stopRecording", () => ({ ...noRecording(), samples: [] }), (value) => ({ ...normalizeHeartRateRecording(value), samples: normalizeHeartRateSamples(hrRecord(value).samples) }), ...options === undefined ? [] : [options]);
|
|
1181
|
+
},
|
|
1182
|
+
async getRecordingState(options) {
|
|
1183
|
+
validateRecordingOwnership("heartRate", options);
|
|
1184
|
+
return heartRateCall("getRecordingState", noRecording, normalizeHeartRateRecording, ...options === undefined ? [] : [options]);
|
|
1185
|
+
},
|
|
1186
|
+
async acknowledgeRecording(options) {
|
|
1187
|
+
if (!options || !Object.prototype.hasOwnProperty.call(options, "expectedId"))
|
|
1188
|
+
throw Object.assign(new Error("Acknowledgement needs expectedId"), { code: "INVALID_ARGUMENT" });
|
|
1189
|
+
validateRecordingOwnership("heartRate", options);
|
|
1190
|
+
return heartRateCall("acknowledgeRecording", () => false, (value) => value === true, options);
|
|
1191
|
+
},
|
|
1192
|
+
async readRecording(options = {}) {
|
|
1193
|
+
validateRecordingOwnership("heartRate", options);
|
|
1194
|
+
return heartRateCall("readRecording", () => [], normalizeHeartRateSamples, options);
|
|
1195
|
+
},
|
|
1046
1196
|
onReading: (callback) => hrSubscribe(heartRateWatch.readings, callback),
|
|
1047
1197
|
onDevice: (callback) => hrSubscribe(heartRateWatch.devices, callback),
|
|
1048
1198
|
onState: (callback) => hrSubscribe(heartRateWatch.states, callback)
|
|
@@ -1646,6 +1796,7 @@ export {
|
|
|
1646
1796
|
watchConnectivity,
|
|
1647
1797
|
tabBar,
|
|
1648
1798
|
symbols,
|
|
1799
|
+
storage,
|
|
1649
1800
|
statusBar,
|
|
1650
1801
|
splash,
|
|
1651
1802
|
speech,
|
|
@@ -1679,6 +1830,7 @@ export {
|
|
|
1679
1830
|
dialog,
|
|
1680
1831
|
device,
|
|
1681
1832
|
deepLinks,
|
|
1833
|
+
crypto,
|
|
1682
1834
|
crashReporter,
|
|
1683
1835
|
contextMenu,
|
|
1684
1836
|
chrome,
|