@craft-native/ios 0.0.237 → 0.0.241
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/dist/cli.js +2 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +17 -1
- package/dist/testing.d.ts +16 -2
- package/package.json +1 -1
- package/templates/CraftApp.swift +418 -93
- package/templates/CraftHeartRate.swift +141 -44
- package/templates/CraftNativeActions.swift +46 -48
- package/templates/CraftNativeScreen.swift +82 -17
- package/templates/CraftPersistentStorage.swift +99 -0
- package/templates/CraftRecordingArchive.swift +85 -0
- package/templates/testing/DriverTests.swift.template +45 -1
|
@@ -136,9 +136,12 @@ final class CraftHeartRate: NSObject, CBCentralManagerDelegate, CBPeripheralDele
|
|
|
136
136
|
static let maxRecordedSamples = 50_000
|
|
137
137
|
|
|
138
138
|
private let clock: () -> Date
|
|
139
|
+
private let recordingDirectory: URL?
|
|
139
140
|
|
|
140
|
-
init(now: @escaping () -> Date = Date.init) {
|
|
141
|
+
init(now: @escaping () -> Date = Date.init, recordingDirectory: URL? = nil, archiveWriter: @escaping CraftRecordingArchive.Writer = CraftRecordingArchive.writeAtomically) {
|
|
141
142
|
self.clock = now
|
|
143
|
+
self.recordingDirectory = recordingDirectory
|
|
144
|
+
self.archiveWriter = archiveWriter
|
|
142
145
|
super.init()
|
|
143
146
|
}
|
|
144
147
|
|
|
@@ -169,6 +172,8 @@ final class CraftHeartRate: NSObject, CBCentralManagerDelegate, CBPeripheralDele
|
|
|
169
172
|
private var recoveryTimer: Timer?
|
|
170
173
|
private var hasSubscribed = false
|
|
171
174
|
|
|
175
|
+
private var archiveWriter: CraftRecordingArchive.Writer = CraftRecordingArchive.writeAtomically
|
|
176
|
+
private var recordingActive = false
|
|
172
177
|
private var recordingId: String?
|
|
173
178
|
private var recordingStartedAt: Double?
|
|
174
179
|
private var recordingPaused = false
|
|
@@ -199,8 +204,53 @@ final class CraftHeartRate: NSObject, CBCentralManagerDelegate, CBPeripheralDele
|
|
|
199
204
|
/// One call from a page or a native screen, by method name.
|
|
200
205
|
func perform(method: String, args: [Any], completion: @escaping Completion) {
|
|
201
206
|
let options = args.first as? [String: Any] ?? [:]
|
|
207
|
+
if method == "acknowledgeRecording" && !options.keys.contains("expectedId") {
|
|
208
|
+
return completion(.failure(CraftHeartRateError(message: "Acknowledgement needs expectedId", code: "INVALID_ARGUMENT")))
|
|
209
|
+
}
|
|
202
210
|
DispatchQueue.main.async {
|
|
203
|
-
self.restoreRecording()
|
|
211
|
+
do { try self.restoreRecording() }
|
|
212
|
+
catch { return completion(.failure(CraftHeartRateError(message: error.localizedDescription, code: "RECORDING_STORAGE_ERROR"))) }
|
|
213
|
+
if ["getRecordingState", "readRecording", "stopRecording", "acknowledgeRecording"].contains(method),
|
|
214
|
+
options.keys.contains("expectedId") {
|
|
215
|
+
guard let id = options["expectedId"] as? String, !id.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
|
216
|
+
return completion(.failure(CraftHeartRateError(message: "expectedId must be a nonempty recording ID", code: "INVALID_ARGUMENT")))
|
|
217
|
+
}
|
|
218
|
+
do {
|
|
219
|
+
if method == "acknowledgeRecording" {
|
|
220
|
+
guard self.recordingId != id || !self.recordingActive else {
|
|
221
|
+
return completion(.failure(CraftHeartRateError(message: "The recording is still active", code: "RECORDING_ALREADY_ACTIVE")))
|
|
222
|
+
}
|
|
223
|
+
var known = try self.recordingId == id || CraftRecordingArchive.isAcknowledged(self.directory, "heart-rate", id)
|
|
224
|
+
if !known { known = try CraftRecordingArchive.read(self.directory, "heart-rate", id) != nil }
|
|
225
|
+
guard known else { return completion(.failure(CraftHeartRateError(message: "Unknown recording ID", code: "RECORDING_CONFLICT"))) }
|
|
226
|
+
try CraftRecordingArchive.acknowledge(self.directory, "heart-rate", id)
|
|
227
|
+
if self.recordingId == id {
|
|
228
|
+
if FileManager.default.fileExists(atPath: self.samplesURL.path) { try FileManager.default.removeItem(at: self.samplesURL) }
|
|
229
|
+
self.recordingId = nil
|
|
230
|
+
self.recordingStartedAt = nil
|
|
231
|
+
self.recordedCount = 0
|
|
232
|
+
try self.persistRecording()
|
|
233
|
+
}
|
|
234
|
+
return completion(.success(true))
|
|
235
|
+
}
|
|
236
|
+
if var archived = try CraftRecordingArchive.read(self.directory, "heart-rate", id) {
|
|
237
|
+
if method == "readRecording" {
|
|
238
|
+
let since = (options["since"] as? NSNumber)?.doubleValue
|
|
239
|
+
let samples = archived["samples"] as? [[String: Any]] ?? []
|
|
240
|
+
return completion(.success(samples.filter { since == nil || (($0["timestamp"] as? NSNumber)?.doubleValue ?? 0) > since! }))
|
|
241
|
+
}
|
|
242
|
+
if method == "getRecordingState" { archived.removeValue(forKey: "samples") }
|
|
243
|
+
return completion(.success(archived))
|
|
244
|
+
}
|
|
245
|
+
if let error = self.recordingIdentityError(options) { return completion(.failure(error)) }
|
|
246
|
+
} catch let error as CraftNativeActionError {
|
|
247
|
+
return completion(.failure(CraftHeartRateError(message: error.message, code: error.code)))
|
|
248
|
+
} catch { return completion(.failure(CraftHeartRateError(message: "Unable to persist the recording", code: "RECORDING_STORAGE_ERROR"))) }
|
|
249
|
+
}
|
|
250
|
+
if ["pauseRecording", "resumeRecording", "stopRecording", "readRecording"].contains(method),
|
|
251
|
+
let error = self.recordingIdentityError(options) {
|
|
252
|
+
return completion(.failure(error))
|
|
253
|
+
}
|
|
204
254
|
switch method {
|
|
205
255
|
case "getState":
|
|
206
256
|
self.recoverQuietBroadcast()
|
|
@@ -214,12 +264,14 @@ final class CraftHeartRate: NSObject, CBCentralManagerDelegate, CBPeripheralDele
|
|
|
214
264
|
case "connect": self.connect(id: options["id"] as? String, wait: true, completion: completion)
|
|
215
265
|
case "reconnect": self.connect(id: nil, wait: false, completion: completion)
|
|
216
266
|
case "disconnect": self.disconnect(forget: options["forget"] as? Bool ?? false, completion: completion)
|
|
217
|
-
case "startRecording": self.startRecording(completion: completion)
|
|
267
|
+
case "startRecording": self.startRecording(options: options, completion: completion)
|
|
218
268
|
case "pauseRecording": self.setRecordingPaused(true, completion: completion)
|
|
219
269
|
case "resumeRecording": self.setRecordingPaused(false, completion: completion)
|
|
220
270
|
case "stopRecording": self.stopRecording(completion: completion)
|
|
221
271
|
case "getRecordingState": completion(.success(self.recordingSummary(count: true)))
|
|
222
|
-
case "readRecording":
|
|
272
|
+
case "readRecording":
|
|
273
|
+
do { completion(.success(try self.persistedSamples(since: (options["since"] as? NSNumber)?.doubleValue))) }
|
|
274
|
+
catch { completion(.failure(CraftHeartRateError(message: "Unable to read the recording", code: "RECORDING_STORAGE_ERROR"))) }
|
|
223
275
|
default: completion(.failure(CraftHeartRateError(message: "Unsupported HeartRate method", code: "UNKNOWN_ACTION")))
|
|
224
276
|
}
|
|
225
277
|
}
|
|
@@ -712,38 +764,46 @@ final class CraftHeartRate: NSObject, CBCentralManagerDelegate, CBPeripheralDele
|
|
|
712
764
|
// MARK: Recording
|
|
713
765
|
|
|
714
766
|
private var directory: URL {
|
|
715
|
-
FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
|
|
767
|
+
recordingDirectory ?? FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
|
|
716
768
|
}
|
|
717
769
|
private var stateURL: URL { directory.appendingPathComponent("craft-heart-rate-recording-state.json") }
|
|
718
770
|
private var samplesURL: URL { directory.appendingPathComponent("craft-heart-rate-recording.jsonl") }
|
|
719
771
|
|
|
720
772
|
/// A recording that was running when the app last closed carries on.
|
|
721
|
-
private func restoreRecording() {
|
|
773
|
+
private func restoreRecording() throws {
|
|
722
774
|
guard !restored else { return }
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
775
|
+
guard FileManager.default.fileExists(atPath: stateURL.path) else {
|
|
776
|
+
if FileManager.default.fileExists(atPath: samplesURL.path), !(try Data(contentsOf: samplesURL)).isEmpty {
|
|
777
|
+
throw CraftNativeActionError(code: "RECORDING_STORAGE_ERROR", message: "Recording metadata is missing")
|
|
778
|
+
}
|
|
779
|
+
restored = true
|
|
780
|
+
return
|
|
781
|
+
}
|
|
782
|
+
let data = try Data(contentsOf: stateURL)
|
|
783
|
+
guard let saved = try JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
784
|
+
let active = saved["active"] as? Bool else {
|
|
785
|
+
throw CraftNativeActionError(code: "RECORDING_STORAGE_ERROR", message: "Invalid recording metadata")
|
|
786
|
+
}
|
|
787
|
+
recordingActive = active
|
|
727
788
|
recordingId = saved["id"] as? String
|
|
728
789
|
recordingStartedAt = saved["startedAt"] as? Double
|
|
729
790
|
recordingPaused = saved["paused"] as? Bool ?? false
|
|
730
|
-
|
|
791
|
+
if let id = recordingId, try CraftRecordingArchive.isAcknowledged(directory, "heart-rate", id) {
|
|
792
|
+
try CraftRecordingArchive.acknowledge(directory, "heart-rate", id)
|
|
793
|
+
if FileManager.default.fileExists(atPath: samplesURL.path) { try FileManager.default.removeItem(at: samplesURL) }
|
|
794
|
+
recordingId = nil
|
|
795
|
+
recordingStartedAt = nil
|
|
796
|
+
recordingActive = false
|
|
797
|
+
recordingPaused = false
|
|
798
|
+
try persistRecording()
|
|
799
|
+
}
|
|
800
|
+
recordedCount = try persistedSamples(since: nil).count
|
|
801
|
+
restored = true
|
|
731
802
|
}
|
|
732
803
|
|
|
733
|
-
private func persistRecording() {
|
|
734
|
-
let saved: [String: Any] = [
|
|
735
|
-
|
|
736
|
-
"paused": recordingPaused,
|
|
737
|
-
"id": recordingId ?? NSNull(),
|
|
738
|
-
"startedAt": recordingStartedAt ?? NSNull(),
|
|
739
|
-
]
|
|
740
|
-
do {
|
|
741
|
-
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
|
742
|
-
try JSONSerialization.data(withJSONObject: saved).write(to: stateURL, options: .atomic)
|
|
743
|
-
protect(stateURL)
|
|
744
|
-
} catch {
|
|
745
|
-
print("Unable to persist the heart-rate recording state: \(error)")
|
|
746
|
-
}
|
|
804
|
+
private func persistRecording() throws {
|
|
805
|
+
let saved: [String: Any] = ["active": recordingActive, "paused": recordingPaused, "id": recordingId ?? NSNull(), "startedAt": recordingStartedAt ?? NSNull()]
|
|
806
|
+
try CraftRecordingArchive.writeAtomically(JSONSerialization.data(withJSONObject: saved), stateURL)
|
|
747
807
|
}
|
|
748
808
|
|
|
749
809
|
private func protect(_ url: URL) {
|
|
@@ -757,7 +817,7 @@ final class CraftHeartRate: NSObject, CBCentralManagerDelegate, CBPeripheralDele
|
|
|
757
817
|
private func recordingSummary(count: Bool) -> [String: Any] {
|
|
758
818
|
var summary: [String: Any] = [
|
|
759
819
|
"id": recordingId ?? NSNull(),
|
|
760
|
-
"active":
|
|
820
|
+
"active": recordingActive,
|
|
761
821
|
"paused": recordingPaused,
|
|
762
822
|
"startedAt": recordingStartedAt ?? NSNull(),
|
|
763
823
|
]
|
|
@@ -765,46 +825,80 @@ final class CraftHeartRate: NSObject, CBCentralManagerDelegate, CBPeripheralDele
|
|
|
765
825
|
return summary
|
|
766
826
|
}
|
|
767
827
|
|
|
768
|
-
private func startRecording(completion: @escaping Completion) {
|
|
828
|
+
private func startRecording(options: [String: Any], completion: @escaping Completion) {
|
|
829
|
+
guard !recordingActive else {
|
|
830
|
+
return completion(.failure(CraftHeartRateError(message: "A heart-rate recording is already active", code: "RECORDING_ALREADY_ACTIVE")))
|
|
831
|
+
}
|
|
832
|
+
let id: String
|
|
769
833
|
do {
|
|
834
|
+
id = try CraftRecordingArchive.startId(options, directory: directory, kind: "heart-rate", currentId: recordingId)
|
|
835
|
+
try CraftRecordingArchive.save(directory, "heart-rate", recordingSummary(count: true), samples: try persistedSamples(since: nil), writer: archiveWriter)
|
|
770
836
|
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
|
771
837
|
try Data().write(to: samplesURL, options: .atomic)
|
|
772
838
|
protect(samplesURL)
|
|
839
|
+
} catch let error as CraftNativeActionError {
|
|
840
|
+
return completion(.failure(CraftHeartRateError(message: error.message, code: error.code)))
|
|
773
841
|
} catch {
|
|
774
842
|
return completion(.failure(CraftHeartRateError(message: "Unable to create the heart-rate recording", code: "RECORDING_STORAGE_ERROR")))
|
|
775
843
|
}
|
|
776
|
-
|
|
844
|
+
let previous = (recordingActive, recordingId, recordingStartedAt, recordingPaused, recordedCount)
|
|
845
|
+
recordingActive = true
|
|
846
|
+
recordingId = id
|
|
777
847
|
recordingStartedAt = (clock().timeIntervalSince1970 * 1000).rounded()
|
|
778
848
|
recordingPaused = false
|
|
779
849
|
recordedCount = 0
|
|
780
|
-
persistRecording()
|
|
850
|
+
do { try persistRecording() }
|
|
851
|
+
catch {
|
|
852
|
+
(recordingActive, recordingId, recordingStartedAt, recordingPaused, recordedCount) = previous
|
|
853
|
+
return completion(.failure(CraftHeartRateError(message: "Unable to persist the recording", code: "RECORDING_STORAGE_ERROR")))
|
|
854
|
+
}
|
|
781
855
|
completion(.success(recordingSummary(count: true)))
|
|
782
856
|
}
|
|
783
857
|
|
|
858
|
+
/// Runs on the same main-queue turn as the read or mutation it guards.
|
|
859
|
+
private func recordingIdentityError(_ options: [String: Any]) -> CraftHeartRateError? {
|
|
860
|
+
guard options.keys.contains("expectedId") else { return nil }
|
|
861
|
+
guard let expected = options["expectedId"] as? String,
|
|
862
|
+
!expected.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
|
863
|
+
return CraftHeartRateError(message: "expectedId must be a nonempty recording ID", code: "INVALID_ARGUMENT")
|
|
864
|
+
}
|
|
865
|
+
guard recordingId == expected else {
|
|
866
|
+
return CraftHeartRateError(message: "The active heart-rate recording has changed", code: "RECORDING_CONFLICT")
|
|
867
|
+
}
|
|
868
|
+
return nil
|
|
869
|
+
}
|
|
870
|
+
|
|
784
871
|
private func setRecordingPaused(_ paused: Bool, completion: @escaping Completion) {
|
|
785
|
-
guard
|
|
872
|
+
guard recordingActive else {
|
|
786
873
|
return completion(.failure(CraftHeartRateError(message: "No active heart-rate recording", code: "NO_ACTIVE_RECORDING")))
|
|
787
874
|
}
|
|
875
|
+
let previous = recordingPaused
|
|
788
876
|
recordingPaused = paused
|
|
789
|
-
persistRecording()
|
|
877
|
+
do { try persistRecording() }
|
|
878
|
+
catch {
|
|
879
|
+
recordingPaused = previous
|
|
880
|
+
return completion(.failure(CraftHeartRateError(message: "Unable to persist the recording", code: "RECORDING_STORAGE_ERROR")))
|
|
881
|
+
}
|
|
790
882
|
completion(.success(recordingSummary(count: true)))
|
|
791
883
|
}
|
|
792
884
|
|
|
793
885
|
private func stopRecording(completion: @escaping Completion) {
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
886
|
+
do {
|
|
887
|
+
let samples = try persistedSamples(since: nil)
|
|
888
|
+
let wasActive = recordingActive
|
|
889
|
+
let wasPaused = recordingPaused
|
|
890
|
+
recordingActive = false
|
|
891
|
+
recordingPaused = false
|
|
892
|
+
do { try persistRecording() }
|
|
893
|
+
catch { recordingActive = wasActive; recordingPaused = wasPaused; throw error }
|
|
894
|
+
var summary = recordingSummary(count: true)
|
|
895
|
+
summary["samples"] = samples
|
|
896
|
+
completion(.success(summary))
|
|
897
|
+
} catch { completion(.failure(CraftHeartRateError(message: "Unable to persist the recording", code: "RECORDING_STORAGE_ERROR"))) }
|
|
804
898
|
}
|
|
805
899
|
|
|
806
900
|
private func appendSample(_ sample: [String: Any]) {
|
|
807
|
-
guard
|
|
901
|
+
guard recordingActive, !recordingPaused, recordedCount < Self.maxRecordedSamples,
|
|
808
902
|
let json = try? JSONSerialization.data(withJSONObject: sample) else { return }
|
|
809
903
|
var line = json
|
|
810
904
|
line.append(0x0A)
|
|
@@ -820,8 +914,11 @@ final class CraftHeartRate: NSObject, CBCentralManagerDelegate, CBPeripheralDele
|
|
|
820
914
|
}
|
|
821
915
|
|
|
822
916
|
/// The recording's samples, oldest first; only those after `since` (ms) when given.
|
|
823
|
-
private func loadSamples(since: Double?) -> [[String: Any]] {
|
|
824
|
-
|
|
917
|
+
private func loadSamples(since: Double?) -> [[String: Any]] { (try? persistedSamples(since: since)) ?? [] }
|
|
918
|
+
|
|
919
|
+
private func persistedSamples(since: Double?) throws -> [[String: Any]] {
|
|
920
|
+
if recordingId == nil { return [] }
|
|
921
|
+
let text = try String(contentsOf: samplesURL, encoding: .utf8)
|
|
825
922
|
return text.split(separator: "\n").compactMap { line in
|
|
826
923
|
guard let data = line.data(using: .utf8),
|
|
827
924
|
let sample = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return nil }
|
|
@@ -42,6 +42,9 @@ enum CraftNativeActions {
|
|
|
42
42
|
// `speech`, `audio` (background sound) and `eventStream` (a fetch read
|
|
43
43
|
// as it arrives) need nothing the app has to declare.
|
|
44
44
|
var values = ["device", "clipboard", "haptics", "storage", "lifecycle", "fetch", "dialog", "speech", "audio", "eventStream", "linking", "badge", "animation"]
|
|
45
|
+
values.append("storageCompareAndSet")
|
|
46
|
+
values.append("storageSetIfAbsent")
|
|
47
|
+
values.append("crypto")
|
|
45
48
|
if config.enableBiometric { values.append("biometric") }
|
|
46
49
|
if config.enableSecureStorage { values.append("secureStorage") }
|
|
47
50
|
if config.enableLocalDatabase { values.append("database") }
|
|
@@ -62,7 +65,11 @@ enum CraftNativeActions {
|
|
|
62
65
|
// GPS and the route recording are the web view's coordinator's
|
|
63
66
|
// (CraftWebServices); an app with no web view has the coordinator
|
|
64
67
|
// all the same (CraftHybrid.configure).
|
|
65
|
-
if config.enableGeolocation {
|
|
68
|
+
if config.enableGeolocation {
|
|
69
|
+
values.append("geolocation")
|
|
70
|
+
if CraftZigRuntime.supportsRecordingOwnership { values.append("locationRecordingOwnership") }
|
|
71
|
+
if CraftZigRuntime.supportsRecordingIntentIdentity { values.append("locationRecordingIntentIdentity") }
|
|
72
|
+
}
|
|
66
73
|
// The Music app's song and transport are the coordinator's too.
|
|
67
74
|
if config.enableAppleMusic { values.append("music") }
|
|
68
75
|
// `craft.background.complete(ok, id)`: the answer to a silent push or
|
|
@@ -70,7 +77,7 @@ enum CraftNativeActions {
|
|
|
70
77
|
if config.backgroundRefresh?.enabled == true || config.enablePushNotifications { values.append("background") }
|
|
71
78
|
// A heart-rate strap is the app's own service (CraftHeartRate.swift),
|
|
72
79
|
// so it needs no web view.
|
|
73
|
-
if config.enableHeartRate { values.append("heartRate") }
|
|
80
|
+
if config.enableHeartRate { values.append(contentsOf: ["heartRate", "heartRateRecordingOwnership", "heartRateRecordingIntentIdentity"]) }
|
|
74
81
|
if config.enableBluetooth { values.append("bluetooth") }
|
|
75
82
|
// Workouts on the paired Apple Watch (CraftScheduledWorkouts.swift),
|
|
76
83
|
// also the app's own service.
|
|
@@ -92,6 +99,8 @@ enum CraftNativeActions {
|
|
|
92
99
|
/// Nil means this small shared router does not own the action.
|
|
93
100
|
static func perform(action: String, body: [String: Any], config: CraftConfig) -> Result<Any, CraftNativeActionError>? {
|
|
94
101
|
switch action {
|
|
102
|
+
case "cryptoRandomUUID":
|
|
103
|
+
return .success(UUID().uuidString.lowercased())
|
|
95
104
|
case "getDeviceInfo":
|
|
96
105
|
return .success(deviceInfo())
|
|
97
106
|
case "haptic":
|
|
@@ -162,6 +171,8 @@ enum CraftNativeActions {
|
|
|
162
171
|
|
|
163
172
|
let sync: Result<Any, CraftNativeActionError>?
|
|
164
173
|
switch (module, method) {
|
|
174
|
+
case ("Crypto", "randomUUID"):
|
|
175
|
+
sync = .success(UUID().uuidString.lowercased())
|
|
165
176
|
case ("Device", "getInfo"):
|
|
166
177
|
sync = perform(action: "getDeviceInfo", body: [:], config: config)
|
|
167
178
|
case ("CrashReporter", "capture"):
|
|
@@ -187,7 +198,8 @@ enum CraftNativeActions {
|
|
|
187
198
|
case ("Clipboard", "read"):
|
|
188
199
|
sync = perform(action: "clipboardRead", body: [:], config: config)
|
|
189
200
|
case ("Storage", _):
|
|
190
|
-
|
|
201
|
+
DispatchQueue.main.async { complete(requestToken, storage(method: method, args: args), completion) }
|
|
202
|
+
return
|
|
191
203
|
case ("SecureStorage", _):
|
|
192
204
|
guard config.enableSecureStorage else {
|
|
193
205
|
complete(requestToken, failure("CAPABILITY_DISABLED", "Secure storage is disabled"), completion)
|
|
@@ -398,6 +410,7 @@ enum CraftNativeActions {
|
|
|
398
410
|
"startRecording": "startLocationRecording", "pauseRecording": "pauseLocationRecording",
|
|
399
411
|
"resumeRecording": "resumeLocationRecording", "stopRecording": "stopLocationRecording",
|
|
400
412
|
"getRecordingState": "getLocationRecordingState", "readRecording": "readLocationRecording",
|
|
413
|
+
"acknowledgeRecording": "acknowledgeLocationRecording",
|
|
401
414
|
]
|
|
402
415
|
guard let action = actions[method] else {
|
|
403
416
|
complete(requestToken, failure("UNKNOWN_ACTION", "Unsupported Location method"), completion)
|
|
@@ -840,44 +853,32 @@ enum CraftNativeActions {
|
|
|
840
853
|
}
|
|
841
854
|
|
|
842
855
|
private static func storage(method: String, args: [Any]) -> Result<Any, CraftNativeActionError> {
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
.
|
|
853
|
-
.
|
|
854
|
-
|
|
855
|
-
}
|
|
856
|
-
guard let key = validKey(args.first) else { return failure("INVALID_ARGUMENT", "Storage needs a non-empty key") }
|
|
857
|
-
let defaultsKey = storagePrefix + key
|
|
858
|
-
switch method {
|
|
859
|
-
case "get":
|
|
860
|
-
guard let data = UserDefaults.standard.data(forKey: defaultsKey) else { return .success(NSNull()) }
|
|
861
|
-
do {
|
|
862
|
-
return .success(try JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed]))
|
|
863
|
-
} catch {
|
|
864
|
-
return failure("STORAGE_CORRUPT", "Stored value is not valid JSON")
|
|
865
|
-
}
|
|
866
|
-
case "set":
|
|
867
|
-
guard args.count == 2 else { return failure("INVALID_ARGUMENT", "Storage.set needs a key and value") }
|
|
868
|
-
do {
|
|
869
|
-
let data = try JSONSerialization.data(withJSONObject: args[1], options: [.fragmentsAllowed])
|
|
870
|
-
UserDefaults.standard.set(data, forKey: defaultsKey)
|
|
856
|
+
do {
|
|
857
|
+
if method == "clear" { try CraftPersistentStorage.clear(prefix: storagePrefix); return .success(true) }
|
|
858
|
+
if method == "keys" { return .success(try CraftPersistentStorage.keys(prefix: storagePrefix)) }
|
|
859
|
+
guard let key = validKey(args.first) else { return failure("INVALID_ARGUMENT", "Storage needs a non-empty key") }
|
|
860
|
+
switch method {
|
|
861
|
+
case "get":
|
|
862
|
+
guard let data = try CraftPersistentStorage.get(key, prefix: storagePrefix) else { return .success(NSNull()) }
|
|
863
|
+
return .success(try JSONSerialization.jsonObject(with: data, options: .fragmentsAllowed))
|
|
864
|
+
case "set":
|
|
865
|
+
guard args.count == 2 else { return failure("INVALID_ARGUMENT", "Storage.set needs a key and value") }
|
|
866
|
+
let data = try JSONSerialization.data(withJSONObject: args[1], options: .fragmentsAllowed)
|
|
867
|
+
try CraftPersistentStorage.set(key, data: data, prefix: storagePrefix)
|
|
871
868
|
return .success(true)
|
|
872
|
-
|
|
873
|
-
return failure("INVALID_ARGUMENT", "Storage
|
|
869
|
+
case "compareAndSet":
|
|
870
|
+
guard args.count == 3 else { return failure("INVALID_ARGUMENT", "Storage.compareAndSet needs a key, expected value and next value") }
|
|
871
|
+
return .success(try CraftPersistentStorage.compareAndSet(key, expected: args[1], value: args[2], prefix: storagePrefix))
|
|
872
|
+
case "setIfAbsent":
|
|
873
|
+
guard args.count == 2 else { return failure("INVALID_ARGUMENT", "Storage.setIfAbsent needs a key and value") }
|
|
874
|
+
return .success(try CraftPersistentStorage.setIfAbsent(key, value: args[1], prefix: storagePrefix))
|
|
875
|
+
case "remove":
|
|
876
|
+
try CraftPersistentStorage.set(key, data: nil, prefix: storagePrefix)
|
|
877
|
+
return .success(true)
|
|
878
|
+
default: return failure("UNKNOWN_ACTION", "Unsupported Storage method")
|
|
874
879
|
}
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
return .success(true)
|
|
878
|
-
default:
|
|
879
|
-
return failure("UNKNOWN_ACTION", "Unsupported Storage method")
|
|
880
|
-
}
|
|
880
|
+
} catch let error as CraftNativeActionError { return .failure(error) }
|
|
881
|
+
catch { return failure("STORAGE_ERROR", "Storage could not persist or read its value") }
|
|
881
882
|
}
|
|
882
883
|
|
|
883
884
|
private static func secureStorage(method: String, args: [Any]) -> Result<Any, CraftNativeActionError> {
|
|
@@ -1389,8 +1390,9 @@ final class CraftNativePasskeys: NSObject, ASAuthorizationControllerDelegate, AS
|
|
|
1389
1390
|
extension CraftNativeActions {
|
|
1390
1391
|
/// The JSON text `craft.storage` keeps under `key`, or nil. The same store
|
|
1391
1392
|
/// as the asynchronous Storage module, read without a bridge round trip.
|
|
1392
|
-
static func storageJSON(forKey key: String) -> String? {
|
|
1393
|
-
guard let key = validKey(key)
|
|
1393
|
+
static func storageJSON(forKey key: String) throws -> String? {
|
|
1394
|
+
guard let key = validKey(key) else { return nil }
|
|
1395
|
+
guard let data = try CraftPersistentStorage.get(key, prefix: storagePrefix) else { return nil }
|
|
1394
1396
|
return String(decoding: data, as: UTF8.self)
|
|
1395
1397
|
}
|
|
1396
1398
|
|
|
@@ -1399,14 +1401,10 @@ extension CraftNativeActions {
|
|
|
1399
1401
|
@discardableResult
|
|
1400
1402
|
static func setStorageJSON(_ json: String?, forKey key: String) -> Bool {
|
|
1401
1403
|
guard let key = validKey(key) else { return false }
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
+
do {
|
|
1405
|
+
try CraftPersistentStorage.set(key, data: json.map { Data($0.utf8) }, prefix: storagePrefix)
|
|
1404
1406
|
return true
|
|
1405
|
-
}
|
|
1406
|
-
let data = Data(json.utf8)
|
|
1407
|
-
guard (try? JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed])) != nil else { return false }
|
|
1408
|
-
UserDefaults.standard.set(data, forKey: storagePrefix + key)
|
|
1409
|
-
return true
|
|
1407
|
+
} catch { return false }
|
|
1410
1408
|
}
|
|
1411
1409
|
|
|
1412
1410
|
/// The Keychain item the web page's `craft.secureStorage` wrote under
|