@sency/react-native-smkit-ui 2.2.0 → 2.2.2

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.
Files changed (30) hide show
  1. package/README.md +218 -65
  2. package/android/build.gradle +3 -1
  3. package/android/src/main/java/com/smkituilibrary/SmkitUiLibraryModule.kt +237 -3
  4. package/android/src/main/java/com/smkituilibrary/mapper/SMMapper.kt +70 -10
  5. package/android/src/main/java/com/smkituilibrary/model/SMKitExercise.kt +40 -0
  6. package/android/src/main/java/com/smkituilibrary/model/SMKitWorkout.kt +8 -0
  7. package/android/src/main/java/com/smkituilibrary/model/SMKitWorkoutConfig.kt +2 -0
  8. package/android/src/main/java/com/smkituilibrary/model/SMUserData.kt +5 -1
  9. package/ios/SMKitUIManager.mm +33 -11
  10. package/ios/SMKitUIManager.swift +243 -26
  11. package/lib/commonjs/SMWorkout.js +239 -177
  12. package/lib/commonjs/SMWorkout.js.map +1 -1
  13. package/lib/commonjs/index.js +187 -5
  14. package/lib/commonjs/index.js.map +1 -1
  15. package/lib/module/SMWorkout.js +232 -181
  16. package/lib/module/SMWorkout.js.map +1 -1
  17. package/lib/module/index.js +165 -5
  18. package/lib/module/index.js.map +1 -1
  19. package/package.json +2 -2
  20. package/react-native-smkit-ui.podspec +2 -2
  21. package/src/SMWorkout.tsx +353 -176
  22. package/src/index.tsx +167 -8
  23. package/android/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar +0 -0
  24. package/android/build/intermediates/compile_symbol_list/debug/generateDebugRFile/R.txt +0 -0
  25. package/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties +0 -1
  26. package/android/build/intermediates/incremental/debug/packageDebugResources/merger.xml +0 -2
  27. package/android/build/intermediates/incremental/mergeDebugJniLibFolders/merger.xml +0 -2
  28. package/android/build/intermediates/local_only_symbol_list/debug/parseDebugLocalResources/R-def.txt +0 -2
  29. package/android/build/intermediates/nested_resources_validation_report/debug/generateDebugResources/nestedResourcesValidationReport.txt +0 -1
  30. package/android/build/intermediates/symbol_list_with_package_name/debug/generateDebugRFile/package-aware-r.txt +0 -1
@@ -18,6 +18,7 @@ class SMKitUIManager: NSObject, RCTBridgeModule {
18
18
 
19
19
  var summaryData: WorkoutSummaryData?
20
20
  var didCompleteWorkout = false
21
+ var startTimerOnFirstActivityOverride: Bool?
21
22
 
22
23
  var didReciveSummary = false {
23
24
  didSet {
@@ -65,7 +66,9 @@ class SMKitUIManager: NSObject, RCTBridgeModule {
65
66
  var isConfigDone = false
66
67
  var didFail = false
67
68
  // RN SDK policy: do not wait for first activity before starting exercise timer.
68
- SMKitUIModel.startTimerOnFirstActivity = false
69
+ SMKitUIModel.startTimerOnFirstActivity = self.startTimerOnFirstActivityOverride ?? false
70
+ // RN SDK policy: keep Jinni wake-word / orb features internal — never enabled from RN.
71
+ SMKitUIModel.alwaysOnJinniWakeWordDuringWorkout = false
69
72
  SMKitUIModel.configure(authKey: "\(key)", includesHighlights: false, onSuccess: {
70
73
  if !isConfigDone {
71
74
  isConfigDone = true
@@ -228,7 +231,9 @@ class SMKitUIManager: NSObject, RCTBridgeModule {
228
231
  difficultyLevel: difficultyLevel,
229
232
  workoutDuration: workoutDuration,
230
233
  language: language,
231
- programID: programID
234
+ programID: programID,
235
+ phonePosition: self.resolvePhonePosition(json["phonePosition"] as? String),
236
+ shortIntro: json["shortIntro"] as? Bool ?? false
232
237
  )
233
238
 
234
239
  SMKitUIModel.startWorkoutFromProgram(viewController: smkitUIViewController, workoutConfig: config, delegate: self, onFailure: { error in
@@ -237,17 +242,19 @@ class SMKitUIManager: NSObject, RCTBridgeModule {
237
242
  }
238
243
  }
239
244
 
240
- @objc(setSessionLanguage:)
241
- func setSessionLanguage(language: String) {
245
+ @objc(setSessionLanguage:onSuccess:onFailure:)
246
+ func setSessionLanguage(language: String, onSuccess: @escaping RCTPromiseResolveBlock, onFailure: @escaping RCTPromiseRejectBlock) {
242
247
  let lang = resolveLanguage(language)
243
248
  SMKitUIModel.setSessionLanguage(language: lang)
244
249
  // Auto-sync phone calibration language so callers don't need to set it separately
245
250
  SMKitUIModel.setPhoneCalibrationLanguage(language: lang)
251
+ onSuccess(nil)
246
252
  }
247
253
 
248
- @objc(setPhoneCalibrationLanguage:)
249
- func setPhoneCalibrationLanguage(language: String) {
254
+ @objc(setPhoneCalibrationLanguage:onSuccess:onFailure:)
255
+ func setPhoneCalibrationLanguage(language: String, onSuccess: @escaping RCTPromiseResolveBlock, onFailure: @escaping RCTPromiseRejectBlock) {
250
256
  SMKitUIModel.setPhoneCalibrationLanguage(language: resolveLanguage(language))
257
+ onSuccess(nil)
251
258
  }
252
259
 
253
260
  private func resolveLanguage(_ language: String) -> SencySupportedLanguage {
@@ -258,29 +265,40 @@ class SMKitUIManager: NSObject, RCTBridgeModule {
258
265
  }
259
266
  }
260
267
 
261
- @objc(setEndExercisePreferences:)
262
- func setEndExercisePreferences(preferencesString: String) {
268
+ private func resolvePhonePosition(_ position: String?) -> PhonePosition {
269
+ switch position?.lowercased() {
270
+ case "elevated": return .Elevated
271
+ default: return .Floor
272
+ }
273
+ }
274
+
275
+ @objc(setEndExercisePreferences:onSuccess:onFailure:)
276
+ func setEndExercisePreferences(preferencesString: String, onSuccess: @escaping RCTPromiseResolveBlock, onFailure: @escaping RCTPromiseRejectBlock) {
263
277
  let target = EndExercisePreferences(rawValue: preferencesString) ?? .Default
264
278
  SMKitUIModel.setEndExercisePreferences(endExercisePreferences: target)
279
+ onSuccess(nil)
265
280
  }
266
281
 
267
- @objc(setCounterPreferences:)
268
- func setCounterPreferences(preferencesString: String) {
282
+ @objc(setCounterPreferences:onSuccess:onFailure:)
283
+ func setCounterPreferences(preferencesString: String, onSuccess: @escaping RCTPromiseResolveBlock, onFailure: @escaping RCTPromiseRejectBlock) {
269
284
  let preferences = CounterPreferences(rawValue: preferencesString) ?? .Default
270
285
  SMKitUIModel.setCounterPreferences(counterPreferences: preferences)
286
+ onSuccess(nil)
271
287
  }
272
288
 
273
- @objc(setIntelligenceRestEnabled:)
274
- func setIntelligenceRestEnabled(_ enabled: Bool) {
289
+ @objc(setIntelligenceRestEnabled:onSuccess:onFailure:)
290
+ func setIntelligenceRestEnabled(_ enabled: Bool, onSuccess: @escaping RCTPromiseResolveBlock, onFailure: @escaping RCTPromiseRejectBlock) {
275
291
  SMKitUIModel.enableIntelligenceRest = enabled
292
+ onSuccess(nil)
276
293
  }
277
294
 
278
- @objc(setSkeletonSettings:)
279
- func setSkeletonSettings(_ configString: String) {
295
+ @objc(setSkeletonSettings:onSuccess:onFailure:)
296
+ func setSkeletonSettings(_ configString: String, onSuccess: @escaping RCTPromiseResolveBlock, onFailure: @escaping RCTPromiseRejectBlock) {
280
297
  guard let data = configString.data(using: .utf8),
281
298
  let rawConfig = try? JSONSerialization.jsonObject(with: data),
282
299
  let config = rawConfig as? [String: Any] else {
283
300
  print("setSkeletonSettings: invalid JSON")
301
+ onFailure("setSkeletonSettings Failed", "Invalid JSON", nil)
284
302
  return
285
303
  }
286
304
  if let v = config["hidden"] as? Bool { SMKitUIModel.skeletonHidden = v }
@@ -299,37 +317,49 @@ class SMKitUIManager: NSObject, RCTBridgeModule {
299
317
  if let v = config["dotsOuterColor"] as? String, let c = skeletonColor(from: v) { SMKitUIModel.skeletonDotsOuterColorOption = c }
300
318
  if let v = config["connectionsInnerColor"] as? String, let c = skeletonColor(from: v) { SMKitUIModel.skeletonConnectionsInnerColorOption = c }
301
319
  if let v = config["connectionsOuterColor"] as? String, let c = skeletonColor(from: v) { SMKitUIModel.skeletonConnectionsOuterColorOption = c }
320
+ onSuccess(nil)
302
321
  }
303
322
 
304
- @objc(setPauseTypes:)
305
- func setPauseTypes(_ typesJson: String) {
323
+ @objc(setPauseTypes:onSuccess:onFailure:)
324
+ func setPauseTypes(_ typesJson: String, onSuccess: @escaping RCTPromiseResolveBlock, onFailure: @escaping RCTPromiseRejectBlock) {
306
325
  guard let data = typesJson.data(using: .utf8),
307
326
  let rawArray = try? JSONSerialization.jsonObject(with: data),
308
- let arr = rawArray as? [String] else { return }
327
+ let arr = rawArray as? [String] else {
328
+ onFailure("setPauseTypes Failed", "Invalid JSON", nil)
329
+ return
330
+ }
309
331
  let types = arr.compactMap { pauseAlertType(from: $0) }
310
- try? SMKitUIModel.setAllowedPauseTypes(types: types)
332
+ do {
333
+ try SMKitUIModel.setAllowedPauseTypes(types: types)
334
+ onSuccess(nil)
335
+ } catch {
336
+ onFailure("setPauseTypes Failed", error.localizedDescription, error)
337
+ }
311
338
  }
312
339
 
313
- @objc(setAllowAudioMixing:)
314
- func setAllowAudioMixing(_ enabled: Bool) {
340
+ @objc(setAllowAudioMixing:onSuccess:onFailure:)
341
+ func setAllowAudioMixing(_ enabled: Bool, onSuccess: @escaping RCTPromiseResolveBlock, onFailure: @escaping RCTPromiseRejectBlock) {
315
342
  SMKitUIModel.allowAudioMixing = enabled
343
+ onSuccess(nil)
316
344
  }
317
345
 
318
- @objc(setShowExternalAudioControl:)
319
- func setShowExternalAudioControl(_ enabled: Bool) {
346
+ @objc(setShowExternalAudioControl:onSuccess:onFailure:)
347
+ func setShowExternalAudioControl(_ enabled: Bool, onSuccess: @escaping RCTPromiseResolveBlock, onFailure: @escaping RCTPromiseRejectBlock) {
320
348
  SMKitUIModel.showExternalAudioControl = enabled
349
+ onSuccess(nil)
321
350
  }
322
351
 
323
- @objc(setAccuratePoseEstimation:)
324
- func setAccuratePoseEstimation(_ enabled: Bool) {
352
+ @objc(setAccuratePoseEstimation:onSuccess:onFailure:)
353
+ func setAccuratePoseEstimation(_ enabled: Bool, onSuccess: @escaping RCTPromiseResolveBlock, onFailure: @escaping RCTPromiseRejectBlock) {
325
354
  SMKitUIModel.accuratePoseEstimation = enabled
355
+ onSuccess(nil)
326
356
  }
327
357
 
328
358
  // Instruction video config (align with ../smkit_android ConfigureViewModel).
329
359
  // displayMode: "default" | "mediumCycle". mediumCycle: video at 75% for first N reps, then 50%.
330
360
  // mediumSizeCycles: 1–5, only used when displayMode is mediumCycle.
331
- @objc(setInstructionVideoConfig:)
332
- func setInstructionVideoConfig(_ configString: String) {
361
+ @objc(setInstructionVideoConfig:onSuccess:onFailure:)
362
+ func setInstructionVideoConfig(_ configString: String, onSuccess: @escaping RCTPromiseResolveBlock, onFailure: @escaping RCTPromiseRejectBlock) {
333
363
  do {
334
364
  let configDict = try stringJsonToDic(stringJSON: configString)
335
365
 
@@ -344,11 +374,177 @@ class SMKitUIManager: NSObject, RCTBridgeModule {
344
374
  SMKitUIModel.instructionVideoConfig = config
345
375
 
346
376
  print("SMKitUIManager: Set InstructionVideoConfig - mode: \(displayModeStr), cycles: \(mediumSizeCycles)")
377
+ onSuccess(nil)
347
378
  } catch {
348
379
  print("SMKitUIManager: Error parsing InstructionVideoConfig - \(error.localizedDescription)")
380
+ onFailure("setInstructionVideoConfig Failed", error.localizedDescription, error)
381
+ }
382
+ }
383
+
384
+ @objc(setColorTheme:onSuccess:onFailure:)
385
+ func setColorTheme(_ theme: String, onSuccess: @escaping RCTPromiseResolveBlock, onFailure: @escaping RCTPromiseRejectBlock) {
386
+ SMKitUIModel.colorTheme = mapColorToTheme(primaryColor: theme)
387
+ onSuccess(nil)
388
+ }
389
+
390
+ @objc(setPlayPhoneCalibrationAudio:onSuccess:onFailure:)
391
+ func setPlayPhoneCalibrationAudio(_ enabled: Bool, onSuccess: @escaping RCTPromiseResolveBlock, onFailure: @escaping RCTPromiseRejectBlock) {
392
+ SMKitUIModel.playPhoneCalibrationAudio = enabled
393
+ onSuccess(nil)
394
+ }
395
+
396
+ @objc(setPlayBodyCalibrationAudio:onSuccess:onFailure:)
397
+ func setPlayBodyCalibrationAudio(_ enabled: Bool, onSuccess: @escaping RCTPromiseResolveBlock, onFailure: @escaping RCTPromiseRejectBlock) {
398
+ SMKitUIModel.playBodyCalibrationAudio = enabled
399
+ onSuccess(nil)
400
+ }
401
+
402
+ @objc(setStartTimerOnFirstActivity:onSuccess:onFailure:)
403
+ func setStartTimerOnFirstActivity(_ enabled: Bool, onSuccess: @escaping RCTPromiseResolveBlock, onFailure: @escaping RCTPromiseRejectBlock) {
404
+ startTimerOnFirstActivityOverride = enabled
405
+ SMKitUIModel.startTimerOnFirstActivity = enabled
406
+ onSuccess(nil)
407
+ }
408
+
409
+ @objc(setWorkoutContinuationTimerDuration:onSuccess:onFailure:)
410
+ func setWorkoutContinuationTimerDuration(_ seconds: NSNumber, onSuccess: @escaping RCTPromiseResolveBlock, onFailure: @escaping RCTPromiseRejectBlock) {
411
+ SMKitUIModel.workoutContinuationTimerDuration = max(1.0, seconds.doubleValue)
412
+ onSuccess(nil)
413
+ }
414
+
415
+ @objc(setPhoneMovementCountPreventionEnabled:onSuccess:onFailure:)
416
+ func setPhoneMovementCountPreventionEnabled(_ enabled: Bool, onSuccess: @escaping RCTPromiseResolveBlock, onFailure: @escaping RCTPromiseRejectBlock) {
417
+ SMKitUIModel.enablePhoneMovementCountPrevention = enabled
418
+ onSuccess(nil)
419
+ }
420
+
421
+ @objc(setVariationMismatchFeedbackEnabled:onSuccess:onFailure:)
422
+ func setVariationMismatchFeedbackEnabled(_ enabled: Bool, onSuccess: @escaping RCTPromiseResolveBlock, onFailure: @escaping RCTPromiseRejectBlock) {
423
+ SMKitUIModel.enableVariationMismatchFeedback = enabled
424
+ onSuccess(nil)
425
+ }
426
+
427
+ @objc(setUseDefaultGuidanceMode:onSuccess:onFailure:)
428
+ func setUseDefaultGuidanceMode(_ enabled: Bool, onSuccess: @escaping RCTPromiseResolveBlock, onFailure: @escaping RCTPromiseRejectBlock) {
429
+ SMKitFlowManager.useDefaultGuidanceMode = enabled
430
+ onSuccess(nil)
431
+ }
432
+
433
+ @objc(setGuidanceDebugLogging:onSuccess:onFailure:)
434
+ func setGuidanceDebugLogging(_ enabled: Bool, onSuccess: @escaping RCTPromiseResolveBlock, onFailure: @escaping RCTPromiseRejectBlock) {
435
+ UserDefaults.standard.set(enabled, forKey: "guidanceDebugLogging")
436
+ onSuccess(nil)
437
+ }
438
+
439
+ @objc(setEnableButtonTutorial:onSuccess:onFailure:)
440
+ func setEnableButtonTutorial(_ enabled: Bool, onSuccess: @escaping RCTPromiseResolveBlock, onFailure: @escaping RCTPromiseRejectBlock) {
441
+ SMKitUIModel.enableButtonTutorial = enabled
442
+ onSuccess(nil)
443
+ }
444
+
445
+ @objc(setButtonTutorialCompletionAudioUri:onSuccess:onFailure:)
446
+ func setButtonTutorialCompletionAudioUri(_ uri: NSString?, onSuccess: @escaping RCTPromiseResolveBlock, onFailure: @escaping RCTPromiseRejectBlock) {
447
+ guard let raw = uri as? String, !raw.isEmpty else {
448
+ SMKitUIModel.buttonTutorialCompletionAudioURL = nil
449
+ onSuccess(nil)
450
+ return
451
+ }
452
+ SMKitUIModel.buttonTutorialCompletionAudioURL = URL(string: raw)
453
+ onSuccess(nil)
454
+ }
455
+
456
+ @objc(setShowRowingPhoneCalibration:onSuccess:onFailure:)
457
+ func setShowRowingPhoneCalibration(_ enabled: Bool, onSuccess: @escaping RCTPromiseResolveBlock, onFailure: @escaping RCTPromiseRejectBlock) {
458
+ SMKitUIModel.showRowingPhoneCalibration = enabled
459
+ onSuccess(nil)
460
+ }
461
+
462
+ @objc(setFeedbacksUIToExclude:onSuccess:onFailure:)
463
+ func setFeedbacksUIToExclude(_ feedbacksJson: String, onSuccess: @escaping RCTPromiseResolveBlock, onFailure: @escaping RCTPromiseRejectBlock) {
464
+ do {
465
+ let feedbacks = try parseFeedbackTypes(feedbacksJson)
466
+ SMKitUIModel.setFeedbacksUIToExclude(feedbacksUIToExclude: feedbacks)
467
+ onSuccess(nil)
468
+ } catch {
469
+ onFailure("setFeedbacksUIToExclude Failed", error.localizedDescription, error)
349
470
  }
350
471
  }
351
472
 
473
+ @objc(setExcludedFeedbacks:onSuccess:onFailure:)
474
+ func setExcludedFeedbacks(_ feedbacksJson: String, onSuccess: @escaping RCTPromiseResolveBlock, onFailure: @escaping RCTPromiseRejectBlock) {
475
+ do {
476
+ let feedbacks = try parseFeedbackTypes(feedbacksJson)
477
+ SMKitUIModel.setExcludedFeedbacks(excludedFeedbacks: feedbacks)
478
+ onSuccess(nil)
479
+ } catch {
480
+ onFailure("setExcludedFeedbacks Failed", error.localizedDescription, error)
481
+ }
482
+ }
483
+
484
+ @objc(setConfigString:onSuccess:onFailure:)
485
+ func setConfigString(_ configString: NSString?, onSuccess: @escaping RCTPromiseResolveBlock, onFailure: @escaping RCTPromiseRejectBlock) {
486
+ // The iOS SMKitUI public surface does not expose a global config-string setter.
487
+ // Exercise modifications remain supported through the start* APIs.
488
+ onSuccess(nil)
489
+ }
490
+
491
+ @objc(clearAdaptiveRomCache:onFailure:)
492
+ func clearAdaptiveRomCache(onSuccess: @escaping RCTPromiseResolveBlock, onFailure: @escaping RCTPromiseRejectBlock) {
493
+ SMKitFlowManager.clearAdaptiveRomPilotCache()
494
+ onSuccess(nil)
495
+ }
496
+
497
+ @objc(quitWorkout:onFailure:)
498
+ func quitWorkout(onSuccess: @escaping RCTPromiseResolveBlock, onFailure: @escaping RCTPromiseRejectBlock) {
499
+ SMKitUIModel.exitSDK()
500
+ onSuccess(nil)
501
+ }
502
+
503
+ @objc(pauseSDK:onFailure:)
504
+ func pauseSDK(onSuccess: @escaping RCTPromiseResolveBlock, onFailure: @escaping RCTPromiseRejectBlock) {
505
+ do {
506
+ try SMKitUIModel.pauseSDK()
507
+ onSuccess(nil)
508
+ } catch {
509
+ onFailure("pauseSDK Failed", error.localizedDescription, error)
510
+ }
511
+ }
512
+
513
+ @objc(resumeSDK:onFailure:)
514
+ func resumeSDK(onSuccess: @escaping RCTPromiseResolveBlock, onFailure: @escaping RCTPromiseRejectBlock) {
515
+ do {
516
+ try SMKitUIModel.resumeSDK()
517
+ onSuccess(nil)
518
+ } catch {
519
+ onFailure("resumeSDK Failed", error.localizedDescription, error)
520
+ }
521
+ }
522
+
523
+ @objc(getSupportedMovements:onFailure:)
524
+ func getSupportedMovements(onSuccess: @escaping RCTPromiseResolveBlock, onFailure: @escaping RCTPromiseRejectBlock) {
525
+ onSuccess(SMKitUIModel.getSupportedMovements() ?? [])
526
+ }
527
+
528
+ @objc(getExerciseType:onSuccess:onFailure:)
529
+ func getExerciseType(_ detector: String, onSuccess: @escaping RCTPromiseResolveBlock, onFailure: @escaping RCTPromiseRejectBlock) {
530
+ do {
531
+ let exerciseType = try SMKitUIModel.getExerciseType(type: detector)
532
+ onSuccess(String(describing: exerciseType))
533
+ } catch {
534
+ onFailure("getExerciseType Failed", error.localizedDescription, error)
535
+ }
536
+ }
537
+
538
+ @objc(setPoseModelChoice:onSuccess:onFailure:)
539
+ func setPoseModelChoice(_ choice: String, onSuccess: @escaping RCTPromiseResolveBlock, onFailure: @escaping RCTPromiseRejectBlock) {
540
+ guard ["AdaptiveChoice", "Prime", "Pro", "Lite", "UltraLite", "Basic"].contains(choice) else {
541
+ onFailure("setPoseModelChoice Failed", "Invalid pose model choice: \(choice)", nil)
542
+ return
543
+ }
544
+ // Android-only. Resolve for API parity.
545
+ onSuccess(nil)
546
+ }
547
+
352
548
  private func pauseAlertType(from s: String) -> PauseAlertType? {
353
549
  switch s {
354
550
  case "resume": return .Resume
@@ -455,6 +651,27 @@ class SMKitUIManager: NSObject, RCTBridgeModule {
455
651
  }
456
652
  }
457
653
 
654
+ private func parseFeedbackTypes(_ feedbacksJson: String) throws -> [FormFeedbackTypeBr] {
655
+ guard let data = feedbacksJson.data(using: .utf8),
656
+ let rawArray = try JSONSerialization.jsonObject(with: data) as? [String] else {
657
+ throw NSError(
658
+ domain: "SMKitUIManager",
659
+ code: 0,
660
+ userInfo: [NSLocalizedDescriptionKey: "Expected a JSON array of feedback type names"]
661
+ )
662
+ }
663
+ return try rawArray.map { raw in
664
+ guard let feedback = FormFeedbackTypeBr(rawValue: raw) else {
665
+ throw NSError(
666
+ domain: "SMKitUIManager",
667
+ code: 0,
668
+ userInfo: [NSLocalizedDescriptionKey: "Invalid feedback type: \(raw)"]
669
+ )
670
+ }
671
+ return feedback
672
+ }
673
+ }
674
+
458
675
  private func stringJsonToDic(stringJSON: String) throws -> [String: Any] {
459
676
  guard let data = stringJSON.data(using: .utf8),
460
677
  let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]