@shenai/capacitor-sdk 3.1.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.
@@ -0,0 +1,1101 @@
1
+ import Foundation
2
+ import Capacitor
3
+ import UIKit
4
+ import WebKit
5
+ import ShenaiSDK // ShenaiSDK.xcframework
6
+ //import ShenaiSDK.ShenaiHealthRisks
7
+ //import ShenaiSDK.ShenaiView
8
+
9
+ private final class ShenaiTouchForwardingRecognizer: UIGestureRecognizer {
10
+ var shouldForwardTouch: ((UITouch) -> Bool)?
11
+ var onTouchesBegan: ((Set<UITouch>, UIEvent?) -> Void)?
12
+ var onTouchesMoved: ((Set<UITouch>, UIEvent?) -> Void)?
13
+ var onTouchesEnded: ((Set<UITouch>, UIEvent?) -> Void)?
14
+ var onTouchesCancelled: ((Set<UITouch>, UIEvent?) -> Void)?
15
+ private var activeForwardedTouches = Set<UITouch>()
16
+
17
+ override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
18
+ let forwardedTouches = Set(touches.filter(shouldStartForwarding))
19
+ if forwardedTouches.isEmpty {
20
+ state = .failed
21
+ return
22
+ }
23
+ activeForwardedTouches.formUnion(forwardedTouches)
24
+ onTouchesBegan?(forwardedTouches, event)
25
+ state = .began
26
+ }
27
+
28
+ override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
29
+ let forwardedTouches = touches.intersection(activeForwardedTouches)
30
+ if !forwardedTouches.isEmpty {
31
+ onTouchesMoved?(forwardedTouches, event)
32
+ }
33
+ state = .changed
34
+ }
35
+
36
+ override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
37
+ let forwardedTouches = touches.intersection(activeForwardedTouches)
38
+ if !forwardedTouches.isEmpty {
39
+ onTouchesEnded?(forwardedTouches, event)
40
+ }
41
+ activeForwardedTouches.subtract(touches)
42
+ state = .ended
43
+ }
44
+
45
+ override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
46
+ let forwardedTouches = touches.intersection(activeForwardedTouches)
47
+ if !forwardedTouches.isEmpty {
48
+ onTouchesCancelled?(forwardedTouches, event)
49
+ }
50
+ activeForwardedTouches.subtract(touches)
51
+ state = .cancelled
52
+ }
53
+
54
+ override func canPrevent(_ preventedGestureRecognizer: UIGestureRecognizer) -> Bool {
55
+ false
56
+ }
57
+
58
+ override func canBePrevented(by preventingGestureRecognizer: UIGestureRecognizer) -> Bool {
59
+ false
60
+ }
61
+
62
+ override func reset() {
63
+ activeForwardedTouches.removeAll()
64
+ super.reset()
65
+ }
66
+
67
+ private func shouldStartForwarding(_ touch: UITouch) -> Bool {
68
+ guard let shouldForwardTouch else {
69
+ return true
70
+ }
71
+ return shouldForwardTouch(touch)
72
+ }
73
+ }
74
+
75
+ // MARK: - Capacitor registration ------------------------------------------------
76
+
77
+ @objc(ShenaiSdkCapacitorPlugin)
78
+ public class ShenaiSdkCapacitorPlugin: CAPPlugin, CAPBridgedPlugin {
79
+
80
+ public let identifier = "ShenaiSdkCapacitorPlugin"
81
+ public let jsName = "ShenaiSdkCapacitor"
82
+
83
+ public let pluginMethods: [CAPPluginMethod] = [
84
+ CAPPluginMethod(name: "echo", returnType: CAPPluginReturnPromise),
85
+ CAPPluginMethod(name: "initialize", returnType: CAPPluginReturnPromise),
86
+ CAPPluginMethod(name: "isInitialized", returnType: CAPPluginReturnPromise),
87
+ CAPPluginMethod(name: "deinitialize", returnType: CAPPluginReturnPromise),
88
+ CAPPluginMethod(name: "setViewRect", returnType: CAPPluginReturnPromise),
89
+ CAPPluginMethod(name: "setOverlaysWebview", returnType: CAPPluginReturnPromise),
90
+ CAPPluginMethod(name: "setOperatingMode", returnType: CAPPluginReturnPromise),
91
+ CAPPluginMethod(name: "startMeasurement", returnType: CAPPluginReturnPromise),
92
+ CAPPluginMethod(name: "stopMeasurement", returnType: CAPPluginReturnPromise),
93
+ CAPPluginMethod(name: "resetMeasurementSession", returnType: CAPPluginReturnPromise),
94
+ CAPPluginMethod(name: "getOperatingMode", returnType: CAPPluginReturnPromise),
95
+ CAPPluginMethod(name: "getCalibrationState", returnType: CAPPluginReturnPromise),
96
+ CAPPluginMethod(name: "setPrecisionMode", returnType: CAPPluginReturnPromise),
97
+ CAPPluginMethod(name: "getPrecisionMode", returnType: CAPPluginReturnPromise),
98
+ CAPPluginMethod(name: "setMeasurementPreset", returnType: CAPPluginReturnPromise),
99
+ CAPPluginMethod(name: "getMeasurementPreset", returnType: CAPPluginReturnPromise),
100
+ CAPPluginMethod(name: "setCameraMode", returnType: CAPPluginReturnPromise),
101
+ CAPPluginMethod(name: "getCameraMode", returnType: CAPPluginReturnPromise),
102
+ CAPPluginMethod(name: "getLastCameraError", returnType: CAPPluginReturnPromise),
103
+ CAPPluginMethod(name: "setScreen", returnType: CAPPluginReturnPromise),
104
+ CAPPluginMethod(name: "getScreen", returnType: CAPPluginReturnPromise),
105
+ CAPPluginMethod(name: "setShowUserInterface", returnType: CAPPluginReturnPromise),
106
+ CAPPluginMethod(name: "getShowUserInterface", returnType: CAPPluginReturnPromise),
107
+ CAPPluginMethod(name: "setShowFacePositioningOverlay", returnType: CAPPluginReturnPromise),
108
+ CAPPluginMethod(name: "getShowFacePositioningOverlay", returnType: CAPPluginReturnPromise),
109
+ CAPPluginMethod(name: "setShowVisualWarnings", returnType: CAPPluginReturnPromise),
110
+ CAPPluginMethod(name: "getShowVisualWarnings", returnType: CAPPluginReturnPromise),
111
+ CAPPluginMethod(name: "setEnableCameraSwap", returnType: CAPPluginReturnPromise),
112
+ CAPPluginMethod(name: "getEnableCameraSwap", returnType: CAPPluginReturnPromise),
113
+ CAPPluginMethod(name: "setShowFaceMask", returnType: CAPPluginReturnPromise),
114
+ CAPPluginMethod(name: "getShowFaceMask", returnType: CAPPluginReturnPromise),
115
+ CAPPluginMethod(name: "setShowBloodFlow", returnType: CAPPluginReturnPromise),
116
+ CAPPluginMethod(name: "getShowBloodFlow", returnType: CAPPluginReturnPromise),
117
+ CAPPluginMethod(name: "setIncludeTimestampInPdf", returnType: CAPPluginReturnPromise),
118
+ CAPPluginMethod(name: "getIncludeTimestampInPdf", returnType: CAPPluginReturnPromise),
119
+ CAPPluginMethod(name: "setPdfEmailSubject", returnType: CAPPluginReturnPromise),
120
+ CAPPluginMethod(name: "setPdfEmailBody", returnType: CAPPluginReturnPromise),
121
+ CAPPluginMethod(name: "setShowStartStopButton", returnType: CAPPluginReturnPromise),
122
+ CAPPluginMethod(name: "getShowStartStopButton", returnType: CAPPluginReturnPromise),
123
+ CAPPluginMethod(name: "setEnableMeasurementsDashboard", returnType: CAPPluginReturnPromise),
124
+ CAPPluginMethod(name: "getEnableMeasurementsDashboard", returnType: CAPPluginReturnPromise),
125
+ CAPPluginMethod(name: "setShowInfoButton", returnType: CAPPluginReturnPromise),
126
+ CAPPluginMethod(name: "getShowInfoButton", returnType: CAPPluginReturnPromise),
127
+ CAPPluginMethod(name: "getShowDisclaimer", returnType: CAPPluginReturnPromise),
128
+ CAPPluginMethod(name: "setEnableStartAfterSuccess", returnType: CAPPluginReturnPromise),
129
+ CAPPluginMethod(name: "getEnableStartAfterSuccess", returnType: CAPPluginReturnPromise),
130
+ CAPPluginMethod(name: "getFaceState", returnType: CAPPluginReturnPromise),
131
+ CAPPluginMethod(name: "getNormalizedFaceBbox", returnType: CAPPluginReturnPromise),
132
+ CAPPluginMethod(name: "getMeasurementState", returnType: CAPPluginReturnPromise),
133
+ CAPPluginMethod(name: "getCurrentViolatedMeasurementEnvironmentCondition", returnType: CAPPluginReturnPromise),
134
+ CAPPluginMethod(name: "isReadyToStartMeasurement", returnType: CAPPluginReturnPromise),
135
+ CAPPluginMethod(name: "areRequiredModelsDownloaded", returnType: CAPPluginReturnPromise),
136
+ CAPPluginMethod(name: "getMeasurementProgressPercentage", returnType: CAPPluginReturnPromise),
137
+ CAPPluginMethod(name: "getHeartRate10s", returnType: CAPPluginReturnPromise),
138
+ CAPPluginMethod(name: "getHeartRate4s", returnType: CAPPluginReturnPromise),
139
+ CAPPluginMethod(name: "getRealtimeMetrics", returnType: CAPPluginReturnPromise),
140
+ CAPPluginMethod(name: "getMeasurementResults", returnType: CAPPluginReturnPromise),
141
+ CAPPluginMethod(name: "getMeasurementResultsHistory",returnType: CAPPluginReturnPromise),
142
+ CAPPluginMethod(name: "getRealtimeHeartbeats", returnType: CAPPluginReturnPromise),
143
+ CAPPluginMethod(name: "getFullPpgSignal", returnType: CAPPluginReturnPromise),
144
+ CAPPluginMethod(name: "setRecordingEnabled", returnType: CAPPluginReturnPromise),
145
+ CAPPluginMethod(name: "getRecordingEnabled", returnType: CAPPluginReturnPromise),
146
+ CAPPluginMethod(name: "getTotalBadSignalSeconds", returnType: CAPPluginReturnPromise),
147
+ CAPPluginMethod(name: "getCurrentSignalQualityMetric", returnType: CAPPluginReturnPromise),
148
+ CAPPluginMethod(name: "getSignalQualityMapPng", returnType: CAPPluginReturnPromise),
149
+ CAPPluginMethod(name: "getFaceTexturePng", returnType: CAPPluginReturnPromise),
150
+ CAPPluginMethod(name: "setCustomMeasurementConfig", returnType: CAPPluginReturnPromise),
151
+ CAPPluginMethod(name: "setCustomColorTheme", returnType: CAPPluginReturnPromise),
152
+ CAPPluginMethod(name: "setLanguage", returnType: CAPPluginReturnPromise),
153
+ CAPPluginMethod(name: "getHealthRisksFactors", returnType: CAPPluginReturnPromise),
154
+ CAPPluginMethod(name: "getHealthRisks", returnType: CAPPluginReturnPromise),
155
+ CAPPluginMethod(name: "computeHealthRisks", returnType: CAPPluginReturnPromise),
156
+ CAPPluginMethod(name: "getMaximalRisks", returnType: CAPPluginReturnPromise),
157
+ CAPPluginMethod(name: "getMinimalRisks", returnType: CAPPluginReturnPromise),
158
+ CAPPluginMethod(name: "getReferenceRisks", returnType: CAPPluginReturnPromise),
159
+ CAPPluginMethod(name: "openMeasurementResultsPdfInBrowser", returnType: CAPPluginReturnPromise),
160
+ CAPPluginMethod(name: "sendMeasurementResultsPdfToEmail", returnType: CAPPluginReturnPromise),
161
+ CAPPluginMethod(name: "requestMeasurementResultsPdfUrl", returnType: CAPPluginReturnPromise),
162
+ CAPPluginMethod(name: "getMeasurementResultsPdfUrl", returnType: CAPPluginReturnPromise),
163
+ CAPPluginMethod(name: "requestMeasurementResultsPdfBytes", returnType: CAPPluginReturnPromise),
164
+ CAPPluginMethod(name: "getMeasurementResultsPdfBytes", returnType: CAPPluginReturnPromise),
165
+ CAPPluginMethod(name: "getResultAsFhirObservation", returnType: CAPPluginReturnPromise),
166
+ CAPPluginMethod(name: "sendResultFhirObservation", returnType: CAPPluginReturnPromise)
167
+ ]
168
+
169
+ // MARK: - State --------------------------------------------------------------
170
+
171
+ private var shenaiView: ShenaiView?
172
+ private var shenaiContainerView: UIView?
173
+ private var touchForwardingRecognizer: ShenaiTouchForwardingRecognizer?
174
+
175
+ // MARK: - Simple echo --------------------------------------------------------
176
+
177
+ @objc func echo(_ call: CAPPluginCall) {
178
+ let value = call.getString("value") ?? ""
179
+ call.resolve(["value": value])
180
+ }
181
+
182
+ // MARK: - INITIALIZATION -----------------------------------------------------
183
+
184
+ @objc func initialize(_ call: CAPPluginCall) {
185
+ guard let apiKey = call.getString("apiKey") else {
186
+ call.reject("apiKey is required"); return
187
+ }
188
+
189
+ let userId = call.getString("userId")
190
+ let settings = InitializationSettings()
191
+
192
+ if let dict = call.getObject("settings") {
193
+
194
+ func bool(_ key: String) -> Bool? { dict[key] as? Bool }
195
+ func int (_ key: String) -> Int? { (dict[key] as? NSNumber)?.intValue }
196
+ func string(_ key: String) -> String? { dict[key] as? String }
197
+
198
+ if let v = int("precisionMode"),
199
+ let e = PrecisionMode(rawValue: v) {
200
+ settings.precisionMode = e
201
+ }
202
+ if let v = int("operatingMode"),
203
+ let e = OperatingMode(rawValue: v) {
204
+ settings.operatingMode = e
205
+ }
206
+ if let v = int("measurementPreset"),
207
+ let e = MeasurementPreset(rawValue: v) {
208
+ settings.measurementPreset = e
209
+ }
210
+ if let v = int("cameraMode"),
211
+ let e = CameraMode(rawValue: v) {
212
+ settings.cameraMode = e
213
+ }
214
+ if let v = int("onboardingMode"),
215
+ let e = OnboardingMode(rawValue: v) {
216
+ settings.onboardingMode = e
217
+ }
218
+ if let v = int("initializationMode"),
219
+ let e = InitializationMode(rawValue: v) {
220
+ settings.initializationMode = e
221
+ }
222
+ if let v = bool("offlineProcessing") {
223
+ settings.offlineProcessing = v
224
+ }
225
+
226
+ if let v = bool("showUserInterface") { settings.showUserInterface = v }
227
+ if let v = bool("showFacePositioningOverlay") { settings.showFacePositioningOverlay = v }
228
+ if let v = bool("showVisualWarnings") { settings.showVisualWarnings = v }
229
+ if let v = bool("enableCameraSwap") { settings.enableCameraSwap = v }
230
+ if let v = bool("showFaceMask") { settings.showFaceMask = v }
231
+ if let v = bool("showBloodFlow") { settings.showBloodFlow = v }
232
+ if let v = bool("hideShenaiLogo") { settings.hideShenaiLogo = v }
233
+ if let v = bool("includeTimestampInPdf") { settings.includeTimestampInPdf = v }
234
+ if let v = string("pdfEmailSubject") { settings.pdfEmailSubject = v }
235
+ if let v = string("pdfEmailBody") { settings.pdfEmailBody = v }
236
+ if let v = bool("enableStartAfterSuccess") { settings.enableStartAfterSuccess = v }
237
+ if let v = bool("enableSummaryScreen") { settings.enableSummaryScreen = v }
238
+ if let v = bool("showResultsFinishButton") { settings.showResultsFinishButton = v }
239
+ if let v = bool("enableHealthRisks") { settings.enableHealthRisks = v }
240
+ if let v = bool("showHealthIndicesFinishButton") {
241
+ settings.showHealthIndicesFinishButton = v
242
+ }
243
+ if let v = bool("saveHealthRisksFactors") { settings.saveHealthRisksFactors = v }
244
+ if let v = bool("showOutOfRangeResultIndicators"){ settings.showOutOfRangeResultIndicators = v }
245
+ if let v = bool("showTrialMetricLabels") { settings.showTrialMetricLabels = v }
246
+ if let v = bool("showSignalQualityIndicator") { settings.showSignalQualityIndicator = v }
247
+ if let v = bool("showSignalTile") { settings.showSignalTile = v }
248
+ if let v = bool("showStartStopButton") { settings.showStartStopButton = v }
249
+ if let v = bool("showInfoButton") { settings.showInfoButton = v }
250
+ if let v = bool("showDisclaimer") { settings.showDisclaimer = v }
251
+ if let v = bool("enableMeasurementsDashboard") { settings.enableMeasurementsDashboard = v }
252
+ if let v = int("uiVersion"),
253
+ let e = UiVersion(rawValue: v) {
254
+ settings.uiVersion = e
255
+ }
256
+ if let v = int("frameWidth") {
257
+ settings.frameWidth = Int32(v)
258
+ }
259
+ if let v = int("frameHeight") {
260
+ settings.frameHeight = Int32(v)
261
+ }
262
+ if let v = int("rotation") {
263
+ settings.rotation = Int32(v)
264
+ }
265
+ if let list = dict["uiFlowScreens"] as? [Int] {
266
+ settings.uiFlowScreens = list.map { NSNumber(value: $0) }
267
+ }
268
+ if let rf = dict["risksFactors"] as? [String: Any] {
269
+ settings.risksFactors = risks(from: rf)
270
+ }
271
+ }
272
+
273
+ settings.eventCallback = { [weak self] event in
274
+ var name = "UNKNOWN"
275
+ switch event {
276
+ case .startButtonClicked: name = "START_BUTTON_CLICKED"
277
+ case .stopButtonClicked: name = "STOP_BUTTON_CLICKED"
278
+ case .measurementFinished: name = "MEASUREMENT_FINISHED"
279
+ case .userFlowFinished: name = "USER_FLOW_FINISHED"
280
+ case .screenChanged: name = "SCREEN_CHANGED"
281
+ @unknown default: break
282
+ }
283
+ self?.notifyListeners("ShenAIEvent", data: ["EventName": name])
284
+ }
285
+
286
+ let result = ShenaiSDK.initialize(apiKey, userID: userId, settings: settings)
287
+
288
+ DispatchQueue.main.async { [weak self] in
289
+ guard let self,
290
+ let host = self.bridge?.viewController,
291
+ let webView = self.bridge?.webView else {
292
+ call.reject("Capacitor bridge view is not available")
293
+ return
294
+ }
295
+ self.installShenaiView(in: host.view, alongside: webView)
296
+ call.resolve(["value": result.rawValue])
297
+ }
298
+ }
299
+
300
+ @objc func isInitialized(_ call: CAPPluginCall) {
301
+ call.resolve(["value": ShenaiSDK.isInitialized()])
302
+ }
303
+
304
+ @objc func deinitialize(_ call: CAPPluginCall) {
305
+ ShenaiSDK.deinitialize()
306
+ DispatchQueue.main.async { [weak self] in
307
+ self?.removeShenaiView()
308
+ if let webView = self?.bridge?.webView {
309
+ self?.restoreOpaqueWebView(webView)
310
+ }
311
+ call.resolve()
312
+ }
313
+ }
314
+
315
+ @objc func setViewRect(_ call: CAPPluginCall) {
316
+ let x = CGFloat(call.getDouble("x") ?? 0.0)
317
+ let y = CGFloat(call.getDouble("y") ?? 0.0)
318
+ let width = CGFloat(call.getDouble("width") ?? 0.0)
319
+ let height = CGFloat(call.getDouble("height") ?? 0.0)
320
+
321
+ DispatchQueue.main.async { [weak self] in
322
+ guard let containerView = self?.shenaiContainerView,
323
+ let parent = containerView.superview else {
324
+ call.reject("View not initialized")
325
+ return
326
+ }
327
+
328
+ let fullWidth = width == 0.0
329
+ let fullHeight = height == 0.0
330
+ containerView.frame = CGRect(
331
+ x: x,
332
+ y: y,
333
+ width: fullWidth ? parent.bounds.width : width,
334
+ height: fullHeight ? parent.bounds.height : height
335
+ )
336
+ containerView.autoresizingMask = fullWidth && fullHeight ? [.flexibleWidth, .flexibleHeight] : []
337
+ self?.shenaiView?.view.frame = containerView.bounds
338
+ call.resolve()
339
+ }
340
+ }
341
+
342
+ @objc func setOverlaysWebview(_ call: CAPPluginCall) {
343
+ guard let overlay = call.getBool("overlay") else {
344
+ call.reject("overlay is required")
345
+ return
346
+ }
347
+
348
+ DispatchQueue.main.async { [weak self] in
349
+ guard let self,
350
+ let containerView = self.shenaiContainerView,
351
+ let webView = self.bridge?.webView,
352
+ let parent = containerView.superview else {
353
+ call.reject("View not initialized")
354
+ return
355
+ }
356
+
357
+ if overlay {
358
+ parent.bringSubviewToFront(containerView)
359
+ } else {
360
+ self.configureTransparentWebView(webView)
361
+ parent.bringSubviewToFront(webView)
362
+ }
363
+ call.resolve()
364
+ }
365
+ }
366
+
367
+ private func installShenaiView(in hostView: UIView, alongside webView: UIView) {
368
+ removeShenaiView()
369
+ configureTransparentWebView(webView)
370
+
371
+ let parent: UIView = webView.superview ?? hostView
372
+ let containerView = UIView(frame: parent.bounds)
373
+ containerView.backgroundColor = .clear
374
+ containerView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
375
+
376
+ let shenaiView = ShenaiView()
377
+ shenaiView.view.frame = containerView.bounds
378
+ shenaiView.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
379
+ containerView.addSubview(shenaiView.view)
380
+
381
+ parent.addSubview(containerView)
382
+ parent.bringSubviewToFront(webView)
383
+
384
+ self.shenaiView = shenaiView
385
+ self.shenaiContainerView = containerView
386
+ installTouchForwardingRecognizer(on: webView)
387
+ }
388
+
389
+ private func removeShenaiView() {
390
+ if let recognizer = touchForwardingRecognizer {
391
+ recognizer.view?.removeGestureRecognizer(recognizer)
392
+ touchForwardingRecognizer = nil
393
+ }
394
+ if let view = shenaiView {
395
+ if view.parent != nil {
396
+ view.willMove(toParent: nil)
397
+ }
398
+ view.view.removeFromSuperview()
399
+ if view.parent != nil {
400
+ view.removeFromParent()
401
+ }
402
+ shenaiView = nil
403
+ }
404
+ shenaiContainerView?.removeFromSuperview()
405
+ shenaiContainerView = nil
406
+ }
407
+
408
+ private func configureTransparentWebView(_ webView: UIView) {
409
+ webView.backgroundColor = .clear
410
+ if let webView = webView as? WKWebView {
411
+ webView.isOpaque = false
412
+ webView.scrollView.backgroundColor = .clear
413
+ }
414
+ }
415
+
416
+ private func restoreOpaqueWebView(_ webView: UIView) {
417
+ webView.backgroundColor = .white
418
+ if let webView = webView as? WKWebView {
419
+ webView.isOpaque = true
420
+ webView.scrollView.backgroundColor = .white
421
+ }
422
+ }
423
+
424
+ private func installTouchForwardingRecognizer(on webView: UIView) {
425
+ let recognizer = ShenaiTouchForwardingRecognizer(target: nil, action: nil)
426
+ recognizer.cancelsTouchesInView = false
427
+ recognizer.delaysTouchesBegan = false
428
+ recognizer.delaysTouchesEnded = false
429
+
430
+ recognizer.shouldForwardTouch = { [weak self] touch in
431
+ guard let containerView = self?.shenaiContainerView else {
432
+ return false
433
+ }
434
+ return containerView.bounds.contains(touch.location(in: containerView))
435
+ }
436
+ recognizer.onTouchesBegan = { [weak self] touches, event in
437
+ self?.shenaiView?.view.touchesBegan(touches, with: event)
438
+ }
439
+ recognizer.onTouchesMoved = { [weak self] touches, event in
440
+ self?.shenaiView?.view.touchesMoved(touches, with: event)
441
+ }
442
+ recognizer.onTouchesEnded = { [weak self] touches, event in
443
+ self?.shenaiView?.view.touchesEnded(touches, with: event)
444
+ }
445
+ recognizer.onTouchesCancelled = { [weak self] touches, event in
446
+ self?.shenaiView?.view.touchesCancelled(touches, with: event)
447
+ }
448
+
449
+ webView.addGestureRecognizer(recognizer)
450
+ touchForwardingRecognizer = recognizer
451
+ }
452
+
453
+ // MARK: - ShenaiSDK thin wrappers -------------------------------------------
454
+
455
+ // Example of the simple one-liners; the pattern is identical for every pair
456
+ // ---------------------------------------------------------------------------
457
+
458
+ @objc func setOperatingMode(_ call: CAPPluginCall) {
459
+ guard let raw = call.getInt("operatingMode"),
460
+ let mode = OperatingMode(rawValue: raw) else {
461
+ call.reject("operatingMode must be a valid enum raw value")
462
+ return
463
+ }
464
+ ShenaiSDK.setOperatingMode(mode)
465
+ call.resolve()
466
+ }
467
+
468
+ @objc func startMeasurement(_ call: CAPPluginCall) {
469
+ ShenaiSDK.startMeasurement()
470
+ call.resolve()
471
+ }
472
+
473
+ @objc func stopMeasurement(_ call: CAPPluginCall) {
474
+ ShenaiSDK.stopMeasurement()
475
+ call.resolve()
476
+ }
477
+
478
+ @objc func resetMeasurementSession(_ call: CAPPluginCall) {
479
+ ShenaiSDK.resetMeasurementSession()
480
+ call.resolve()
481
+ }
482
+
483
+ @objc func getOperatingMode(_ call: CAPPluginCall) {
484
+ call.resolve(["value": ShenaiSDK.getOperatingMode().rawValue])
485
+ }
486
+
487
+ @objc func getCalibrationState(_ call: CAPPluginCall) {
488
+ call.resolve(["value": ShenaiSDK.getCalibrationState()])
489
+ }
490
+
491
+ @objc func setPrecisionMode(_ call: CAPPluginCall) {
492
+ guard let raw = call.getInt("precisionMode"),
493
+ let mode = PrecisionMode(rawValue: raw) else {
494
+ call.reject("precisionMode must be a valid enum raw value"); return
495
+ }
496
+ ShenaiSDK.setPrecisionMode(mode)
497
+ call.resolve()
498
+ }
499
+
500
+ @objc func getPrecisionMode(_ call: CAPPluginCall) {
501
+ call.resolve(["value": ShenaiSDK.getPrecisionMode().rawValue])
502
+ }
503
+
504
+
505
+ @objc func setMeasurementPreset(_ call: CAPPluginCall) {
506
+ guard let raw = call.getInt("preset"),
507
+ let preset = MeasurementPreset(rawValue: raw) else {
508
+ call.reject("preset must be a valid enum raw value"); return
509
+ }
510
+ ShenaiSDK.setMeasurementPreset(preset)
511
+ call.resolve()
512
+ }
513
+
514
+ @objc func getMeasurementPreset(_ call: CAPPluginCall) {
515
+ call.resolve(["value": ShenaiSDK.getMeasurementPreset().rawValue])
516
+ }
517
+
518
+ @objc func setCameraMode(_ call: CAPPluginCall) {
519
+ guard let raw = call.getInt("cameraMode"),
520
+ let mode = CameraMode(rawValue: raw) else {
521
+ call.reject("cameraMode must be a valid enum raw value"); return
522
+ }
523
+ ShenaiSDK.setCameraMode(mode)
524
+ call.resolve()
525
+ }
526
+
527
+ @objc func getCameraMode(_ call: CAPPluginCall) {
528
+ call.resolve(["value": ShenaiSDK.getCameraMode().rawValue])
529
+ }
530
+
531
+ @objc func getLastCameraError(_ call: CAPPluginCall) {
532
+ call.resolve(["value": ShenaiSDK.getLastCameraError() as Any? ?? NSNull()])
533
+ }
534
+
535
+ @objc func setScreen(_ call: CAPPluginCall) {
536
+ guard let raw = call.getInt("screen"), let screen = Screen(rawValue: raw) else {
537
+ call.reject("screen must be a valid enum raw value"); return
538
+ }
539
+ ShenaiSDK.setScreen(screen)
540
+ call.resolve()
541
+ }
542
+
543
+ @objc func getScreen(_ call: CAPPluginCall) {
544
+ call.resolve(["value": ShenaiSDK.getScreen().rawValue])
545
+ }
546
+
547
+ // --------------------------------------------------------------------------
548
+ // BOOL FLAGS (straight through)
549
+ // --------------------------------------------------------------------------
550
+ @objc func setShowUserInterface(_ call: CAPPluginCall) {
551
+ if let v = call.getBool("value") {
552
+ ShenaiSDK.setShowUserInterface(v)
553
+ }
554
+ call.resolve()
555
+ }
556
+
557
+ @objc func getShowUserInterface(_ call: CAPPluginCall) {
558
+ call.resolve(["value": ShenaiSDK.getShowUserInterface()])
559
+ }
560
+
561
+ // … replicate the same tiny pattern for every set/get Bool pair:
562
+ // getShowFacePositioningOverlay, setEnableCameraSwap, etc.
563
+ // --------------------------------------------------------------------------
564
+
565
+ @objc func getMeasurementState(_ call: CAPPluginCall) {
566
+ call.resolve(["value": ShenaiSDK.getMeasurementState().rawValue])
567
+ }
568
+
569
+ @objc func getCurrentViolatedMeasurementEnvironmentCondition(_ call: CAPPluginCall) {
570
+ if let value = ShenaiSDK.getCurrentViolatedMeasurementEnvironmentCondition() {
571
+ call.resolve(["value": value.intValue])
572
+ } else {
573
+ call.resolve(["value": NSNull()])
574
+ }
575
+ }
576
+
577
+ @objc func isReadyToStartMeasurement(_ call: CAPPluginCall) {
578
+ call.resolve(["value": ShenaiSDK.isReadyToStartMeasurement()])
579
+ }
580
+
581
+ @objc func areRequiredModelsDownloaded(_ call: CAPPluginCall) {
582
+ call.resolve(["value": ShenaiSDK.areRequiredModelsDownloaded()])
583
+ }
584
+
585
+ // --------------------------------------------------------------------------
586
+ // FULL SIGNAL / BYTES / ARRAYS (unchanged, helpers exist)
587
+ // --------------------------------------------------------------------------
588
+ @objc func getFullPpgSignal(_ call: CAPPluginCall) {
589
+ call.resolve(["value": ShenaiSDK.getFullPPGSignal()])
590
+ }
591
+
592
+ @objc func setRecordingEnabled(_ call: CAPPluginCall) {
593
+ ShenaiSDK.setRecordingEnabled(call.getBool("enabled") ?? false)
594
+ call.resolve()
595
+ }
596
+
597
+ @objc func getRecordingEnabled(_ call: CAPPluginCall) {
598
+ call.resolve(["value": ShenaiSDK.isRecordingEnabled()])
599
+ }
600
+
601
+ // -------------------------------------------------------------------------
602
+ // ADDITIONAL BOOL FLAGS
603
+ // -------------------------------------------------------------------------
604
+ @objc func setShowFacePositioningOverlay(_ call: CAPPluginCall) {
605
+ if let v = call.getBool("value") { ShenaiSDK.setShowFacePositioningOverlay(v) }
606
+ call.resolve()
607
+ }
608
+
609
+ @objc func getShowFacePositioningOverlay(_ call: CAPPluginCall) {
610
+ call.resolve(["value": ShenaiSDK.getShowFacePositioningOverlay()])
611
+ }
612
+
613
+ @objc func setShowVisualWarnings(_ call: CAPPluginCall) {
614
+ if let v = call.getBool("value") { ShenaiSDK.setShowVisualWarnings(v) }
615
+ call.resolve()
616
+ }
617
+
618
+ @objc func getShowVisualWarnings(_ call: CAPPluginCall) {
619
+ call.resolve(["value": ShenaiSDK.getShowVisualWarnings()])
620
+ }
621
+
622
+ @objc func setEnableCameraSwap(_ call: CAPPluginCall) {
623
+ if let v = call.getBool("value") { ShenaiSDK.setEnableCameraSwap(v) }
624
+ call.resolve()
625
+ }
626
+
627
+ @objc func getEnableCameraSwap(_ call: CAPPluginCall) {
628
+ call.resolve(["value": ShenaiSDK.getEnableCameraSwap()])
629
+ }
630
+
631
+ @objc func setShowFaceMask(_ call: CAPPluginCall) {
632
+ if let v = call.getBool("value") { ShenaiSDK.setShowFaceMask(v) }
633
+ call.resolve()
634
+ }
635
+
636
+ @objc func getShowFaceMask(_ call: CAPPluginCall) {
637
+ call.resolve(["value": ShenaiSDK.getShowFaceMask()])
638
+ }
639
+
640
+ @objc func setShowBloodFlow(_ call: CAPPluginCall) {
641
+ if let v = call.getBool("value") { ShenaiSDK.setShowBloodFlow(v) }
642
+ call.resolve()
643
+ }
644
+
645
+ @objc func getShowBloodFlow(_ call: CAPPluginCall) {
646
+ call.resolve(["value": ShenaiSDK.getShowBloodFlow()])
647
+ }
648
+
649
+ @objc func setIncludeTimestampInPdf(_ call: CAPPluginCall) {
650
+ if let v = call.getBool("value") { ShenaiSDK.setIncludeTimestampInPdf(v) }
651
+ call.resolve()
652
+ }
653
+
654
+ @objc func getIncludeTimestampInPdf(_ call: CAPPluginCall) {
655
+ call.resolve(["value": ShenaiSDK.getIncludeTimestampInPdf()])
656
+ }
657
+
658
+ @objc func setPdfEmailSubject(_ call: CAPPluginCall) {
659
+ ShenaiSDK.setPdfEmailSubject(call.getString("subject") ?? "")
660
+ call.resolve()
661
+ }
662
+
663
+ @objc func setPdfEmailBody(_ call: CAPPluginCall) {
664
+ ShenaiSDK.setPdfEmailBody(call.getString("body") ?? "")
665
+ call.resolve()
666
+ }
667
+
668
+ @objc func setShowStartStopButton(_ call: CAPPluginCall) {
669
+ if let v = call.getBool("value") { ShenaiSDK.setShowStartStopButton(v) }
670
+ call.resolve()
671
+ }
672
+
673
+ @objc func getShowStartStopButton(_ call: CAPPluginCall) {
674
+ call.resolve(["value": ShenaiSDK.getShowStartStopButton()])
675
+ }
676
+
677
+ @objc func setEnableMeasurementsDashboard(_ call: CAPPluginCall) {
678
+ if let v = call.getBool("value") { ShenaiSDK.setEnableMeasurementsDashboard(v) }
679
+ call.resolve()
680
+ }
681
+
682
+ @objc func getEnableMeasurementsDashboard(_ call: CAPPluginCall) {
683
+ call.resolve(["value": ShenaiSDK.getEnableMeasurementsDashboard()])
684
+ }
685
+
686
+ @objc func setShowInfoButton(_ call: CAPPluginCall) {
687
+ if let v = call.getBool("value") { ShenaiSDK.setShowInfoButton(v) }
688
+ call.resolve()
689
+ }
690
+
691
+ @objc func getShowInfoButton(_ call: CAPPluginCall) {
692
+ call.resolve(["value": ShenaiSDK.getShowInfoButton()])
693
+ }
694
+
695
+ @objc func getShowDisclaimer(_ call: CAPPluginCall) {
696
+ call.resolve(["value": ShenaiSDK.getShowDisclaimer()])
697
+ }
698
+
699
+ @objc func setEnableStartAfterSuccess(_ call: CAPPluginCall) {
700
+ if let v = call.getBool("value") { ShenaiSDK.setEnableStartAfterSuccess(v) }
701
+ call.resolve()
702
+ }
703
+
704
+ @objc func getEnableStartAfterSuccess(_ call: CAPPluginCall) {
705
+ call.resolve(["value": ShenaiSDK.getEnableStartAfterSuccess()])
706
+ }
707
+
708
+ // -------------------------------------------------------------------------
709
+ // MEASUREMENT STATUS / RESULTS
710
+ // -------------------------------------------------------------------------
711
+
712
+ @objc func getFaceState(_ call: CAPPluginCall) {
713
+ call.resolve(["value": ShenaiSDK.getFaceState().rawValue])
714
+ }
715
+
716
+ @objc func getNormalizedFaceBbox(_ call: CAPPluginCall) {
717
+ if let box = ShenaiSDK.getNormalizedFaceBbox() {
718
+ call.resolve(["x": box.x, "y": box.y, "width": box.width, "height": box.height])
719
+ } else {
720
+ call.resolve()
721
+ }
722
+ }
723
+
724
+ @objc func getMeasurementProgressPercentage(_ call: CAPPluginCall) {
725
+ call.resolve(["value": ShenaiSDK.getMeasurementProgressPercentage()])
726
+ }
727
+
728
+ @objc func getHeartRate10s(_ call: CAPPluginCall) {
729
+ if let hr = ShenaiSDK.getHeartRate10s() { call.resolve(["value": hr]) } else { call.resolve() }
730
+ }
731
+
732
+ @objc func getHeartRate4s(_ call: CAPPluginCall) {
733
+ if let hr = ShenaiSDK.getHeartRate4s() { call.resolve(["value": hr]) } else { call.resolve() }
734
+ }
735
+
736
+ @objc func getRealtimeMetrics(_ call: CAPPluginCall) {
737
+ let period = call.getDouble("periodSec") ?? 1.0
738
+ if let r = ShenaiSDK.getRealtimeMetrics(period) {
739
+ call.resolve(["value": dict(from: r)])
740
+ } else {
741
+ call.resolve()
742
+ }
743
+ }
744
+
745
+ @objc func getMeasurementResults(_ call: CAPPluginCall) {
746
+ if let r = ShenaiSDK.getMeasurementResults() {
747
+ call.resolve(["value": dict(from: r)])
748
+ } else {
749
+ call.resolve()
750
+ }
751
+ }
752
+
753
+ @objc func getMeasurementResultsHistory(_ call: CAPPluginCall) {
754
+ if let hist = ShenaiSDK.getMeasurementResultsHistory() {
755
+ let arr = hist.history.map { ["measurementResults": dict(from: $0.measurementResults),
756
+ "epochTimestamp": $0.epochTimestamp,
757
+ "isCalibration": $0.isCalibration] }
758
+ call.resolve(["value": arr])
759
+ } else {
760
+ call.resolve()
761
+ }
762
+ }
763
+
764
+ @objc func getRealtimeHeartbeats(_ call: CAPPluginCall) {
765
+ let period = call.getDouble("periodSec")
766
+ let arr = ShenaiSDK.getRealtimeHeartbeats(period == nil ? nil : NSNumber(value: period!))
767
+ .map { ["startLocationSec": $0.startLocationSec, "endLocationSec": $0.endLocationSec, "durationMs": $0.durationMs] }
768
+ call.resolve(["value": arr])
769
+ }
770
+
771
+ @objc func getTotalBadSignalSeconds(_ call: CAPPluginCall) {
772
+ call.resolve(["value": ShenaiSDK.getTotalBadSignalSeconds()])
773
+ }
774
+
775
+ @objc func getCurrentSignalQualityMetric(_ call: CAPPluginCall) {
776
+ call.resolve(["value": ShenaiSDK.getCurrentSignalQualityMetric()])
777
+ }
778
+
779
+ @objc func getSignalQualityMapPng(_ call: CAPPluginCall) {
780
+ if let data = ShenaiSDK.getSignalQualityMapPng() {
781
+ let bytes = [UInt8](data)
782
+ call.resolve(["value": bytes])
783
+ } else { call.resolve() }
784
+ }
785
+
786
+ @objc func getFaceTexturePng(_ call: CAPPluginCall) {
787
+ if let data = ShenaiSDK.getFaceTexturePng() {
788
+ let bytes = [UInt8](data)
789
+ call.resolve(["value": bytes])
790
+ } else { call.resolve() }
791
+ }
792
+
793
+ @objc func setCustomMeasurementConfig(_ call: CAPPluginCall) {
794
+ guard let dict = call.getObject("config") else { call.resolve(); return }
795
+ let c = CustomMeasurementConfig()
796
+ c.durationSeconds = dict["durationSeconds"] as? NSNumber
797
+ c.infiniteMeasurement = dict["infiniteMeasurement"] as? Bool ?? false
798
+ c.instantMetrics = dict["instantMetrics"] as? [NSNumber]
799
+ c.summaryMetrics = dict["summaryMetrics"] as? [NSNumber]
800
+ c.healthIndices = dict["healthIndices"] as? [NSNumber]
801
+ c.realtimeHrPeriodSeconds = dict["realtimeHrPeriodSeconds"] as? NSNumber
802
+ c.realtimeHrvPeriodSeconds = dict["realtimeHrvPeriodSeconds"] as? NSNumber
803
+ c.realtimeCardiacStressPeriodSeconds = dict["realtimeCardiacStressPeriodSeconds"] as? NSNumber
804
+ ShenaiSDK.setCustomMeasurementConfig(c)
805
+ call.resolve()
806
+ }
807
+
808
+ @objc func setCustomColorTheme(_ call: CAPPluginCall) {
809
+ guard let dict = call.getObject("theme") else { call.resolve(); return }
810
+ let t = CustomColorTheme()
811
+ t.themeColor = dict["themeColor"] as? String ?? ""
812
+ t.textColor = dict["textColor"] as? String ?? ""
813
+ t.backgroundColor = dict["backgroundColor"] as? String ?? ""
814
+ t.tileColor = dict["tileColor"] as? String ?? ""
815
+ t.buttonMainColor = dict["buttonMainColor"] as? String ?? ""
816
+ t.buttonSecondaryColor = dict["buttonSecondaryColor"] as? String ?? ""
817
+ ShenaiSDK.setCustomColorTheme(t)
818
+ call.resolve()
819
+ }
820
+
821
+ @objc func setLanguage(_ call: CAPPluginCall) {
822
+ if let lang = call.getString("language") { ShenaiSDK.setLanguage(lang) }
823
+ call.resolve()
824
+ }
825
+
826
+ @objc func getHealthRisksFactors(_ call: CAPPluginCall) {
827
+ let factors = ShenaiHealthRisks.getFactors()
828
+ call.resolve(["value": dict(from: factors)])
829
+ }
830
+
831
+ @objc func getHealthRisks(_ call: CAPPluginCall) {
832
+ let risks = ShenaiHealthRisks.getHealthRisks()
833
+ call.resolve(["value": dict(from: risks)])
834
+ }
835
+
836
+ @objc func computeHealthRisks(_ call: CAPPluginCall) {
837
+ let factors = risks(from: call.getObject("risksFactors"))
838
+ let risks = ShenaiHealthRisks.computeHealthRisks(factors)
839
+ call.resolve(["value": dict(from: risks)])
840
+ }
841
+
842
+ @objc func getMaximalRisks(_ call: CAPPluginCall) {
843
+ let factors = risks(from: call.getObject("risksFactors"))
844
+ let risks = ShenaiHealthRisks.getMaximalRisks(factors)
845
+ call.resolve(["value": dict(from: risks)])
846
+ }
847
+
848
+ @objc func getMinimalRisks(_ call: CAPPluginCall) {
849
+ let factors = risks(from: call.getObject("risksFactors"))
850
+ let risks = ShenaiHealthRisks.getMinimalRisks(factors)
851
+ call.resolve(["value": dict(from: risks)])
852
+ }
853
+
854
+ @objc func getReferenceRisks(_ call: CAPPluginCall) {
855
+ let factors = risks(from: call.getObject("risksFactors"))
856
+ let risks = ShenaiHealthRisks.getReferenceRisks(factors)
857
+ call.resolve(["value": dict(from: risks)])
858
+ }
859
+
860
+ @objc func openMeasurementResultsPdfInBrowser(_ call: CAPPluginCall) {
861
+ ShenaiSDK.openMeasurementResultsPdfInBrowser()
862
+ call.resolve()
863
+ }
864
+
865
+ @objc func sendMeasurementResultsPdfToEmail(_ call: CAPPluginCall) {
866
+ if let email = call.getString("email") { ShenaiSDK.sendMeasurementResultsPdf(toEmail: email) }
867
+ call.resolve()
868
+ }
869
+
870
+ @objc func requestMeasurementResultsPdfUrl(_ call: CAPPluginCall) {
871
+ ShenaiSDK.requestMeasurementResultsPdfUrl()
872
+ call.resolve()
873
+ }
874
+
875
+ @objc func getMeasurementResultsPdfUrl(_ call: CAPPluginCall) {
876
+ if let url = ShenaiSDK.getMeasurementResultsPdfUrl() {
877
+ call.resolve(["value": url])
878
+ } else { call.resolve() }
879
+ }
880
+
881
+ @objc func requestMeasurementResultsPdfBytes(_ call: CAPPluginCall) {
882
+ ShenaiSDK.requestMeasurementResultsPdfBytes()
883
+ call.resolve()
884
+ }
885
+
886
+ @objc func getMeasurementResultsPdfBytes(_ call: CAPPluginCall) {
887
+ if let data = ShenaiSDK.getMeasurementResultsPdfBytes() {
888
+ call.resolve(["valueBase64": data.base64EncodedString()])
889
+ } else { call.resolve() }
890
+ }
891
+
892
+ @objc func getResultAsFhirObservation(_ call: CAPPluginCall) {
893
+ if let observation = ShenaiSDK.getResultAsFhirObservation(), !observation.isEmpty {
894
+ call.resolve(["value": observation])
895
+ } else {
896
+ call.resolve()
897
+ }
898
+ }
899
+
900
+ @objc func sendResultFhirObservation(_ call: CAPPluginCall) {
901
+ guard let url = call.getString("url"), !url.isEmpty else {
902
+ call.reject("url is required")
903
+ return
904
+ }
905
+
906
+ ShenaiSDK.sendResultFhirObservation(url) { response in
907
+ if let response = response, !response.isEmpty {
908
+ call.resolve(["value": response])
909
+ } else {
910
+ call.resolve(["value": NSNull()])
911
+ }
912
+ }
913
+ }
914
+
915
+
916
+ // …repeat the same pattern for all the remaining ≈70 methods …
917
+ // (translation is mechanical: replace `call getXXX` with Swift variant,
918
+ // convert Objective-C structs/objects to dictionaries, then resolve)
919
+
920
+ // MARK: - Helpers -----------------------------------------------------------
921
+
922
+ private func dict(from risks: RisksFactors) -> [String: Any] {
923
+ var d: [String: Any] = [:]
924
+ if let v = risks.age { d["age"] = v }
925
+ if let v = risks.cholesterol { d["cholesterol"] = v }
926
+ if let v = risks.cholesterolHDL { d["cholesterolHdl"] = v }
927
+ if let v = risks.sbp { d["sbp"] = v }
928
+ if let v = risks.dbp { d["dbp"] = v }
929
+ if let v = risks.isSmoker { d["isSmoker"] = v }
930
+ d["hypertensionTreatment"] = risks.hypertensionTreatment
931
+ if let v = risks.hasDiabetes { d["hasDiabetes"] = v }
932
+ if let v = risks.bodyHeight { d["bodyHeight"] = v }
933
+ if let v = risks.bodyWeight { d["bodyWeight"] = v }
934
+ if let v = risks.waistCircumference { d["waistCircumference"] = v }
935
+ if let v = risks.neckCircumference { d["neckCircumference"] = v }
936
+ if let v = risks.hipCircumference { d["hipCircumference"] = v }
937
+ d["gender"] = risks.gender
938
+ d["physicalActivity"] = risks.physicalActivity
939
+ if let v = risks.country { d["country"] = v }
940
+ d["race"] = risks.race
941
+ if let v = risks.vegetableFruitDiet { d["vegetableFruitDiet"] = v }
942
+ if let v = risks.historyOfHighGlucose { d["historyOfHighGlucose"] = v }
943
+ if let v = risks.historyOfHypertension { d["historyOfHypertension"] = v }
944
+ if let v = risks.triglyceride { d["triglyceride"] = v }
945
+ if let v = risks.fastingGlucose { d["fastingGlucose"] = v }
946
+ d["familyDiabetes"] = risks.familyDiabetes
947
+ d["parentalHypertension"] = risks.parentalHypertension
948
+ return d
949
+ }
950
+
951
+ private func risks(from dict: [String: Any]?) -> RisksFactors {
952
+ let f = RisksFactors()
953
+ guard let d = dict else { return f }
954
+
955
+ // Optionals that really are NSNumber in the SDK
956
+ f.age = d["age"] as? NSNumber
957
+ f.cholesterol = d["cholesterol"] as? NSNumber
958
+ f.cholesterolHDL = d["cholesterolHdl"] as? NSNumber
959
+ f.sbp = d["sbp"] as? NSNumber
960
+ f.dbp = d["dbp"] as? NSNumber
961
+ f.isSmoker = d["isSmoker"] as? NSNumber
962
+ f.hasDiabetes = d["hasDiabetes"] as? NSNumber
963
+ f.bodyHeight = d["bodyHeight"] as? NSNumber
964
+ f.bodyWeight = d["bodyWeight"] as? NSNumber
965
+ f.waistCircumference = d["waistCircumference"] as? NSNumber
966
+ f.neckCircumference = d["neckCircumference"] as? NSNumber
967
+ f.hipCircumference = d["hipCircumference"] as? NSNumber
968
+ f.vegetableFruitDiet = d["vegetableFruitDiet"] as? NSNumber
969
+ f.historyOfHighGlucose = d["historyOfHighGlucose"] as? NSNumber
970
+ f.historyOfHypertension = d["historyOfHypertension"] as? NSNumber
971
+ f.triglyceride = d["triglyceride"] as? NSNumber
972
+ f.fastingGlucose = d["fastingGlucose"] as? NSNumber
973
+
974
+ // Enum-backed Ints – convert through rawValue
975
+ if let n = (d["hypertensionTreatment"] as? NSNumber)?.intValue,
976
+ let e = HypertensionTreatment(rawValue: n) {
977
+ f.hypertensionTreatment = e
978
+ }
979
+
980
+ if let n = (d["gender"] as? NSNumber)?.intValue,
981
+ let e = Gender(rawValue: n) {
982
+ f.gender = e
983
+ }
984
+
985
+ if let n = (d["physicalActivity"] as? NSNumber)?.intValue,
986
+ let e = PhysicalActivity(rawValue: n) {
987
+ f.physicalActivity = e
988
+ }
989
+
990
+ if let n = (d["race"] as? NSNumber)?.intValue,
991
+ let e = Race(rawValue: n) {
992
+ f.race = e
993
+ }
994
+
995
+ if let n = (d["familyDiabetes"] as? NSNumber)?.intValue,
996
+ let e = FamilyHistory(rawValue: n) {
997
+ f.familyDiabetes = e
998
+ }
999
+
1000
+ if let n = (d["parentalHypertension"] as? NSNumber)?.intValue,
1001
+ let e = ParentalHistory(rawValue: n) {
1002
+ f.parentalHypertension = e
1003
+ }
1004
+
1005
+ return f
1006
+ }
1007
+
1008
+ private func dict(from heartbeat: Heartbeat) -> [String: Any] {
1009
+ [
1010
+ "startLocationSec": heartbeat.startLocationSec,
1011
+ "endLocationSec": heartbeat.endLocationSec,
1012
+ "durationMs": heartbeat.durationMs
1013
+ ]
1014
+ }
1015
+
1016
+ private func dict(from qualityMetrics: MeasurementQualityMetrics?) -> [String: Any]? {
1017
+ guard let qualityMetrics = qualityMetrics else { return nil }
1018
+ var d: [String: Any] = [:]
1019
+ if let v = qualityMetrics.ppgQualityIndex { d["ppgQualityIndex"] = v }
1020
+ if let v = qualityMetrics.bcgQualityIndex { d["bcgQualityIndex"] = v }
1021
+ if let v = qualityMetrics.breathingQualityIndex { d["breathingQualityIndex"] = v }
1022
+ if let v = qualityMetrics.bloodPressureQualityIndex { d["bloodPressureQualityIndex"] = v }
1023
+ if let v = qualityMetrics.expectedSbpMedianAbsErrorMmhg { d["expectedSbpMedianAbsErrorMmhg"] = v }
1024
+ if let v = qualityMetrics.expectedSbpP80AbsErrorMmhg { d["expectedSbpP80AbsErrorMmhg"] = v }
1025
+ if let v = qualityMetrics.expectedSbpMeanAbsErrorMmhg { d["expectedSbpMeanAbsErrorMmhg"] = v }
1026
+ if let v = qualityMetrics.expectedSbpBalancedMaeMmhg { d["expectedSbpBalancedMaeMmhg"] = v }
1027
+ if let v = qualityMetrics.expectedDbpMedianAbsErrorMmhg { d["expectedDbpMedianAbsErrorMmhg"] = v }
1028
+ if let v = qualityMetrics.expectedDbpP80AbsErrorMmhg { d["expectedDbpP80AbsErrorMmhg"] = v }
1029
+ if let v = qualityMetrics.expectedDbpMeanAbsErrorMmhg { d["expectedDbpMeanAbsErrorMmhg"] = v }
1030
+ if let v = qualityMetrics.expectedDbpBalancedMaeMmhg { d["expectedDbpBalancedMaeMmhg"] = v }
1031
+ return d
1032
+ }
1033
+
1034
+ private func dict(from results: MeasurementResults) -> [String: Any] {
1035
+ var d: [String: Any] = [
1036
+ "heartRateBpm": results.heartRateBpm,
1037
+ "bmiCategory": results.bmiCategory.rawValue,
1038
+ "averageSignalQuality": results.averageSignalQuality,
1039
+ "heartbeats": results.heartbeats.map { dict(from: $0) }
1040
+ ]
1041
+ if let v = results.hrvSdnnMs { d["hrvSdnnMs"] = v }
1042
+ if let v = results.hrvLnrmssdMs { d["hrvLnrmssdMs"] = v }
1043
+ if let v = results.stressIndex { d["stressIndex"] = v }
1044
+ if let v = results.parasympatheticActivity { d["parasympatheticActivity"] = v }
1045
+ if let v = results.breathingRateBpm { d["breathingRateBpm"] = v }
1046
+ if let v = results.systolicBloodPressureMmhg { d["systolicBloodPressureMmhg"] = v }
1047
+ if let v = results.diastolicBloodPressureMmhg { d["diastolicBloodPressureMmhg"] = v }
1048
+ if let v = results.cardiacWorkloadMmhgPerSec { d["cardiacWorkloadMmhgPerSec"] = v }
1049
+ if let v = results.ageYears { d["ageYears"] = v }
1050
+ if let v = results.bmiKgPerM2 { d["bmiKgPerM2"] = v }
1051
+ if let v = results.weightKg { d["weightKg"] = v }
1052
+ if let v = results.heightCm { d["heightCm"] = v }
1053
+ if let qualityMetrics = dict(from: results.qualityMetrics) {
1054
+ d["qualityMetrics"] = qualityMetrics
1055
+ } else {
1056
+ d["qualityMetrics"] = NSNull()
1057
+ }
1058
+ return d
1059
+ }
1060
+
1061
+ private func dict(from healthRisks: HealthRisks) -> [String: Any] {
1062
+ var d: [String: Any] = [
1063
+ "hardAndFatalEvents": [
1064
+ "coronaryDeathEventRisk": healthRisks.hardAndFatalEvents.coronaryDeathEventRisk as Any,
1065
+ "fatalStrokeEventRisk": healthRisks.hardAndFatalEvents.fatalStrokeEventRisk as Any,
1066
+ "totalCvMortalityRisk": healthRisks.hardAndFatalEvents.totalCvMortalityRisk as Any,
1067
+ "hardCvEventRisk": healthRisks.hardAndFatalEvents.hardCvEventRisk as Any
1068
+ ],
1069
+ "cvDiseases": [
1070
+ "overallRisk": healthRisks.cvDiseases.overallRisk as Any,
1071
+ "coronaryHeartDiseaseRisk": healthRisks.cvDiseases.coronaryHeartDiseaseRisk as Any,
1072
+ "strokeRisk": healthRisks.cvDiseases.strokeRisk as Any,
1073
+ "heartFailureRisk": healthRisks.cvDiseases.heartFailureRisk as Any,
1074
+ "peripheralVascularDiseaseRisk": healthRisks.cvDiseases.peripheralVascularDiseaseRisk as Any
1075
+ ],
1076
+ "scores": [
1077
+ "ageScore": healthRisks.scores.ageScore as Any,
1078
+ "sbpScore": healthRisks.scores.sbpScore as Any,
1079
+ "smokingScore": healthRisks.scores.smokingScore as Any,
1080
+ "diabetesScore": healthRisks.scores.diabetesScore as Any,
1081
+ "bmiScore": healthRisks.scores.bmiScore as Any,
1082
+ "cholesterolScore": healthRisks.scores.cholesterolScore as Any,
1083
+ "cholesterolHdlScore": healthRisks.scores.cholesterolHdlScore as Any,
1084
+ "totalScore": healthRisks.scores.totalScore as Any
1085
+ ]
1086
+ ]
1087
+ if let v = healthRisks.wellnessScore { d["wellnessScore"] = v }
1088
+ if let v = healthRisks.vascularAge { d["vascularAge"] = v }
1089
+ if let v = healthRisks.bodyFatPercentage { d["bodyFatPercentage"] = v }
1090
+ if let v = healthRisks.basalMetabolicRate { d["basalMetabolicRate"] = v }
1091
+ if let v = healthRisks.waistToHeightRatio { d["waistToHeightRatio"] = v }
1092
+ if let v = healthRisks.bodyRoundnessIndex { d["bodyRoundnessIndex"] = v }
1093
+ if let v = healthRisks.conicityIndex { d["conicityIndex"] = v }
1094
+ if let v = healthRisks.aBodyShapeIndex { d["aBodyShapeIndex"] = v }
1095
+ if let v = healthRisks.totalDailyEnergyExpenditure { d["totalDailyEnergyExpenditure"] = v }
1096
+ if let v = healthRisks.hypertensionRisk { d["hypertensionRisk"] = v }
1097
+ if let v = healthRisks.diabetesRisk { d["diabetesRisk"] = v }
1098
+ d["nonAlcoholicFattyLiverDiseaseRisk"] = healthRisks.nonAlcoholicFattyLiverDiseaseRisk.rawValue
1099
+ return d
1100
+ }
1101
+ }