@flomentumsolutions/capacitor-health-extended 0.0.6 → 0.0.8

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.
@@ -10,8 +10,11 @@ Pod::Spec.new do |s|
10
10
  s.homepage = package['repository']['url']
11
11
  s.author = package['author']
12
12
  s.source = { :git => package['repository']['url'], :tag => s.version.to_s }
13
- s.source_files = 'ios/Sources/**/*.{swift,h,m,c,cc,mm,cpp}'
13
+ # Only include Swift/Obj-C source files that belong to the plugin
14
+ s.source_files = 'ios/Sources/HealthPluginPlugin/**/*.{swift,h,m}'
14
15
  s.ios.deployment_target = '13.0'
15
- s.dependency 'Capacitor'
16
- s.swift_version = '5.1'
17
- end
16
+ s.dependency 'Capacitor', '~> 6.2'
17
+ s.dependency 'CapacitorCordova', '~> 6.2'
18
+ # Match the Swift shipped with Xcode 16 (use 5.9 for Xcode 15.x)
19
+ s.swift_version = '6.0'
20
+ end
package/README.md CHANGED
@@ -35,14 +35,17 @@ npx cap sync
35
35
  </queries>
36
36
 
37
37
  <!-- Declare permissions you’ll request -->
38
- <uses-permission android:name="android.permission.INTERNET" />
39
- <uses-permission android:name="android.permission.health.READ_STEPS"/>
40
- <uses-permission android:name="android.permission.health.READ_EXERCISE"/>
38
+ <uses-permission android:name="android.permission.health.READ_STEPS" />
39
+ <uses-permission android:name="android.permission.health.READ_ACTIVE_CALORIES_BURNED" />
40
+ <uses-permission android:name="android.permission.health.READ_TOTAL_CALORIES_BURNED" />
41
+ <uses-permission android:name="android.permission.health.READ_DISTANCE" />
42
+ <uses-permission android:name="android.permission.health.READ_EXERCISE" />
43
+ <uses-permission android:name="android.permission.health.READ_EXERCISE_ROUTE" />
44
+ <uses-permission android:name="android.permission.health.READ_HEART_RATE" />
41
45
  <uses-permission android:name="android.permission.health.READ_WEIGHT" />
42
- <uses-permission android:name="android.permission.health.READ_DISTANCE"/>
43
- <uses-permission android:name="android.permission.health.READ_ACTIVE_CALORIES_BURNED"/>
44
- <uses-permission android:name="android.permission.health.READ_TOTAL_CALORIES_BURNED"/>
45
- <uses-permission android:name="android.permission.health.READ_HEART_RATE"/>
46
+ <uses-permission android:name="android.permission.health.READ_HEIGHT" />
47
+ <uses-permission android:name="android.permission.health.READ_HEART_RATE_VARIABILITY" />
48
+ <uses-permission android:name="android.permission.health.READ_BLOOD_PRESSURE" />
46
49
  ```
47
50
 
48
51
 
@@ -277,12 +280,12 @@ Query workouts
277
280
 
278
281
  #### QueryAggregatedRequest
279
282
 
280
- | Prop | Type |
281
- | --------------- | ---------------------------------- |
282
- | **`startDate`** | <code>string</code> |
283
- | **`endDate`** | <code>string</code> |
284
- | **`dataType`** | <code>'steps' \| 'calories'</code> |
285
- | **`bucket`** | <code>string</code> |
283
+ | Prop | Type |
284
+ | --------------- | --------------------------------------------------------------------------------------- |
285
+ | **`startDate`** | <code>string</code> |
286
+ | **`endDate`** | <code>string</code> |
287
+ | **`dataType`** | <code>'steps' \| 'active-calories' \| 'mindfulness' \| 'hrv' \| 'blood-pressure'</code> |
288
+ | **`bucket`** | <code>string</code> |
286
289
 
287
290
 
288
291
  #### QueryWorkoutResponse
@@ -303,6 +306,7 @@ Query workouts
303
306
  | **`id`** | <code>string</code> |
304
307
  | **`duration`** | <code>number</code> |
305
308
  | **`distance`** | <code>number</code> |
309
+ | **`steps`** | <code>number</code> |
306
310
  | **`calories`** | <code>number</code> |
307
311
  | **`sourceBundleId`** | <code>string</code> |
308
312
  | **`route`** | <code>RouteSample[]</code> |
@@ -335,6 +339,7 @@ Query workouts
335
339
  | **`endDate`** | <code>string</code> |
336
340
  | **`includeHeartRate`** | <code>boolean</code> |
337
341
  | **`includeRoute`** | <code>boolean</code> |
342
+ | **`includeSteps`** | <code>boolean</code> |
338
343
 
339
344
 
340
345
  ### Type Aliases
@@ -342,6 +347,6 @@ Query workouts
342
347
 
343
348
  #### HealthPermission
344
349
 
345
- <code>'READ_STEPS' | 'READ_WORKOUTS' | 'READ_CALORIES' | 'READ_DISTANCE' | 'READ_HEART_RATE' | 'READ_ROUTE'</code>
350
+ <code>'READ_STEPS' | 'READ_WORKOUTS' | 'READ_ACTIVE_CALORIES' | 'READ_TOTAL_CALORIES' | 'READ_DISTANCE' | 'READ_WEIGHT' | 'READ_HEIGHT' | 'READ_HEART_RATE' | 'READ_ROUTE' | 'READ_MINDFULNESS' | 'READ_HRV' | 'READ_BLOOD_PRESSURE'</code>
346
351
 
347
352
  </docgen-api>
@@ -51,6 +51,7 @@ enum class CapHealthPermission {
51
51
  permissions = [
52
52
  Permission(alias = "READ_STEPS", strings = ["android.permission.health.READ_STEPS"]),
53
53
  Permission(alias = "READ_WEIGHT", strings = ["android.permission.health.READ_WEIGHT"]),
54
+ Permission(alias = "READ_HEIGHT", strings = ["android.permission.health.READ_HEIGHT"]),
54
55
  Permission(alias = "READ_WORKOUTS", strings = ["android.permission.health.READ_EXERCISE"]),
55
56
  Permission(alias = "READ_DISTANCE", strings = ["android.permission.health.READ_DISTANCE"]),
56
57
  Permission(alias = "READ_ACTIVE_CALORIES", strings = ["android.permission.health.READ_ACTIVE_CALORIES_BURNED"]),
@@ -51,7 +51,7 @@ export interface HealthPlugin {
51
51
  */
52
52
  queryWorkouts(request: QueryWorkoutRequest): Promise<QueryWorkoutResponse>;
53
53
  }
54
- export declare type HealthPermission = 'READ_STEPS' | 'READ_WORKOUTS' | 'READ_ACTIVE_CALORIES' | 'READ_TOTAL_CALORIES' | 'READ_DISTANCE' | 'READ_HEART_RATE' | 'READ_ROUTE' | 'READ_MINDFULNESS' | 'READ_HRV' | 'READ_BLOOD_PRESSURE';
54
+ export declare type HealthPermission = 'READ_STEPS' | 'READ_WORKOUTS' | 'READ_ACTIVE_CALORIES' | 'READ_TOTAL_CALORIES' | 'READ_DISTANCE' | 'READ_WEIGHT' | 'READ_HEIGHT' | 'READ_HEART_RATE' | 'READ_ROUTE' | 'READ_MINDFULNESS' | 'READ_HRV' | 'READ_BLOOD_PRESSURE';
55
55
  export interface PermissionsRequest {
56
56
  permissions: HealthPermission[];
57
57
  }
@@ -1 +1 @@
1
- {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["export interface HealthPlugin {\n /**\n * Checks if health API is available.\n * Android: If false is returned, the Google Health Connect app is probably not installed.\n * See showHealthConnectInPlayStore()\n *\n */\n isHealthAvailable(): Promise<{ available: boolean }>;\n\n /**\n * Android only: Returns for each given permission, if it was granted by the underlying health API\n * @param permissions permissions to query\n */\n checkHealthPermissions(permissions: PermissionsRequest): Promise<PermissionResponse>;\n\n /**\n * Requests the permissions from the user.\n *\n * Android: Apps can ask only a few times for permissions, after that the user has to grant them manually in\n * the Health Connect app. See openHealthConnectSettings()\n *\n * iOS: If the permissions are already granted or denied, this method will just return without asking the user. In iOS\n * we can't really detect if a user granted or denied a permission. The return value reflects the assumption that all\n * permissions were granted.\n *\n * @param permissions permissions to request\n */\n requestHealthPermissions(permissions: PermissionsRequest): Promise<PermissionResponse>;\n\n /**\n * Opens the apps settings, which is kind of wrong, because health permissions are configured under:\n * Settings > Apps > (Apple) Health > Access and Devices > [app-name]\n * But we can't go there directly.\n */\n openAppleHealthSettings(): Promise<void>;\n\n /**\n * Opens the Google Health Connect app\n */\n openHealthConnectSettings(): Promise<void>;\n\n /**\n * Opens the Google Health Connect app in PlayStore\n */\n showHealthConnectInPlayStore(): Promise<void>;\n\n /**\n * Query aggregated data\n * @param request\n */\n queryAggregated(request: QueryAggregatedRequest): Promise<QueryAggregatedResponse>;\n\n /**\n * Query workouts\n * @param request\n */\n queryWorkouts(request: QueryWorkoutRequest): Promise<QueryWorkoutResponse>;\n}\n\nexport declare type HealthPermission =\n | 'READ_STEPS'\n | 'READ_WORKOUTS'\n | 'READ_ACTIVE_CALORIES'\n | 'READ_TOTAL_CALORIES'\n | 'READ_DISTANCE'\n | 'READ_HEART_RATE'\n | 'READ_ROUTE'\n | 'READ_MINDFULNESS'\n | 'READ_HRV'\n | 'READ_BLOOD_PRESSURE';\n\nexport interface PermissionsRequest {\n permissions: HealthPermission[];\n}\n\nexport interface PermissionResponse {\n permissions: { [key: string]: boolean }[];\n}\n\nexport interface QueryWorkoutRequest {\n startDate: string;\n endDate: string;\n includeHeartRate: boolean;\n includeRoute: boolean;\n includeSteps: boolean;\n}\n\nexport interface HeartRateSample {\n timestamp: string;\n bpm: number;\n}\n\nexport interface RouteSample {\n timestamp: string;\n lat: number;\n lng: number;\n alt?: number;\n}\n\nexport interface QueryWorkoutResponse {\n workouts: Workout[];\n}\n\nexport interface Workout {\n startDate: string;\n endDate: string;\n workoutType: string;\n sourceName: string;\n id?: string;\n duration: number;\n distance?: number;\n steps?: number;\n calories: number;\n sourceBundleId: string;\n route?: RouteSample[];\n heartRate?: HeartRateSample[];\n}\n\nexport interface QueryAggregatedRequest {\n startDate: string;\n endDate: string;\n dataType: 'steps' | 'active-calories' | 'mindfulness' | 'hrv' | 'blood-pressure';\n bucket: string;\n}\n\nexport interface QueryAggregatedResponse {\n aggregatedData: AggregatedSample[];\n}\n\nexport interface AggregatedSample {\n startDate: string;\n endDate: string;\n value: number;\n}\n"]}
1
+ {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["export interface HealthPlugin {\n /**\n * Checks if health API is available.\n * Android: If false is returned, the Google Health Connect app is probably not installed.\n * See showHealthConnectInPlayStore()\n *\n */\n isHealthAvailable(): Promise<{ available: boolean }>;\n\n /**\n * Android only: Returns for each given permission, if it was granted by the underlying health API\n * @param permissions permissions to query\n */\n checkHealthPermissions(permissions: PermissionsRequest): Promise<PermissionResponse>;\n\n /**\n * Requests the permissions from the user.\n *\n * Android: Apps can ask only a few times for permissions, after that the user has to grant them manually in\n * the Health Connect app. See openHealthConnectSettings()\n *\n * iOS: If the permissions are already granted or denied, this method will just return without asking the user. In iOS\n * we can't really detect if a user granted or denied a permission. The return value reflects the assumption that all\n * permissions were granted.\n *\n * @param permissions permissions to request\n */\n requestHealthPermissions(permissions: PermissionsRequest): Promise<PermissionResponse>;\n\n /**\n * Opens the apps settings, which is kind of wrong, because health permissions are configured under:\n * Settings > Apps > (Apple) Health > Access and Devices > [app-name]\n * But we can't go there directly.\n */\n openAppleHealthSettings(): Promise<void>;\n\n /**\n * Opens the Google Health Connect app\n */\n openHealthConnectSettings(): Promise<void>;\n\n /**\n * Opens the Google Health Connect app in PlayStore\n */\n showHealthConnectInPlayStore(): Promise<void>;\n\n /**\n * Query aggregated data\n * @param request\n */\n queryAggregated(request: QueryAggregatedRequest): Promise<QueryAggregatedResponse>;\n\n /**\n * Query workouts\n * @param request\n */\n queryWorkouts(request: QueryWorkoutRequest): Promise<QueryWorkoutResponse>;\n}\n\nexport declare type HealthPermission =\n | 'READ_STEPS'\n | 'READ_WORKOUTS'\n | 'READ_ACTIVE_CALORIES'\n | 'READ_TOTAL_CALORIES'\n | 'READ_DISTANCE'\n | 'READ_WEIGHT'\n | 'READ_HEIGHT'\n | 'READ_HEART_RATE'\n | 'READ_ROUTE'\n | 'READ_MINDFULNESS'\n | 'READ_HRV'\n | 'READ_BLOOD_PRESSURE';\n\nexport interface PermissionsRequest {\n permissions: HealthPermission[];\n}\n\nexport interface PermissionResponse {\n permissions: { [key: string]: boolean }[];\n}\n\nexport interface QueryWorkoutRequest {\n startDate: string;\n endDate: string;\n includeHeartRate: boolean;\n includeRoute: boolean;\n includeSteps: boolean;\n}\n\nexport interface HeartRateSample {\n timestamp: string;\n bpm: number;\n}\n\nexport interface RouteSample {\n timestamp: string;\n lat: number;\n lng: number;\n alt?: number;\n}\n\nexport interface QueryWorkoutResponse {\n workouts: Workout[];\n}\n\nexport interface Workout {\n startDate: string;\n endDate: string;\n workoutType: string;\n sourceName: string;\n id?: string;\n duration: number;\n distance?: number;\n steps?: number;\n calories: number;\n sourceBundleId: string;\n route?: RouteSample[];\n heartRate?: HeartRateSample[];\n}\n\nexport interface QueryAggregatedRequest {\n startDate: string;\n endDate: string;\n dataType: 'steps' | 'active-calories' | 'mindfulness' | 'hrv' | 'blood-pressure';\n bucket: string;\n}\n\nexport interface QueryAggregatedResponse {\n aggregatedData: AggregatedSample[];\n}\n\nexport interface AggregatedSample {\n startDate: string;\n endDate: string;\n value: number;\n}\n"]}
@@ -6,6 +6,7 @@ import HealthKit
6
6
  * Please read the Capacitor iOS Plugin Development Guide
7
7
  * here: https://capacitorjs.com/docs/plugins/ios
8
8
  */
9
+ @MainActor
9
10
  @objc(HealthPlugin)
10
11
  public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
11
12
  public let identifier = "HealthPlugin"
@@ -22,6 +23,9 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
22
23
 
23
24
  let healthStore = HKHealthStore()
24
25
 
26
+ /// Serial queue to make route‑location mutations thread‑safe without locks
27
+ private let routeSyncQueue = DispatchQueue(label: "com.flomentum.healthplugin.routeSync")
28
+
25
29
  @objc func isHealthAvailable(_ call: CAPPluginCall) {
26
30
  let isAvailable = HKHealthStore.isHealthDataAvailable()
27
31
  call.resolve(["available": isAvailable])
@@ -148,6 +152,14 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
148
152
  return HKObjectType.quantityType(forIdentifier: .stepCount)
149
153
  case "hrv":
150
154
  return HKObjectType.quantityType(forIdentifier: .heartRateVariabilitySDNN)
155
+ case "height":
156
+ return HKObjectType.quantityType(forIdentifier: .height)
157
+ case "distance":
158
+ return HKObjectType.quantityType(forIdentifier: .distanceWalkingRunning)
159
+ case "active-calories":
160
+ return HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)
161
+ case "total-calories":
162
+ return HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)
151
163
  case "blood-pressure":
152
164
  return nil // handled above
153
165
  default:
@@ -183,8 +195,13 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
183
195
  unit = .gramUnit(with: .kilo)
184
196
  } else if dataTypeString == "hrv" {
185
197
  unit = HKUnit.secondUnit(with: .milli)
198
+ } else if dataTypeString == "distance" {
199
+ unit = HKUnit.meter()
200
+ } else if dataTypeString == "active-calories" || dataTypeString == "total-calories" {
201
+ unit = HKUnit.kilocalorie()
202
+ } else if dataTypeString == "height" {
203
+ unit = HKUnit.meter()
186
204
  }
187
-
188
205
  let value = quantitySample.quantity.doubleValue(for: unit)
189
206
  let timestamp = quantitySample.startDate.timeIntervalSince1970 * 1000
190
207
 
@@ -216,6 +233,13 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
216
233
  return [HKObjectType.quantityType(forIdentifier: .stepCount)].compactMap{$0}
217
234
  case "READ_WEIGHT":
218
235
  return [HKObjectType.quantityType(forIdentifier: .bodyMass)].compactMap{$0}
236
+ case "READ_HEIGHT":
237
+ return [HKObjectType.quantityType(forIdentifier: .height)].compactMap { $0 }
238
+ case "READ_TOTAL_CALORIES":
239
+ return [
240
+ HKObjectType.quantityType(forIdentifier: .activeEnergyBurned),
241
+ HKObjectType.quantityType(forIdentifier: .basalEnergyBurned) // iOS 16+
242
+ ].compactMap { $0 }
219
243
  case "READ_ACTIVE_CALORIES":
220
244
  return [HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)].compactMap{$0}
221
245
  case "READ_WORKOUTS":
@@ -257,6 +281,12 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
257
281
  return HKObjectType.quantityType(forIdentifier: .bodyMass)
258
282
  case "hrv":
259
283
  return HKObjectType.quantityType(forIdentifier: .heartRateVariabilitySDNN)
284
+ case "distance":
285
+ return HKObjectType.quantityType(forIdentifier: .distanceWalkingRunning) // pick one rep type
286
+ case "total-calories":
287
+ return HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)
288
+ case "height":
289
+ return HKObjectType.quantityType(forIdentifier: .height)
260
290
  default:
261
291
  return nil
262
292
  }
@@ -296,12 +326,29 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
296
326
  }
297
327
 
298
328
  let options: HKStatisticsOptions = {
299
- switch dataType.identifier {
300
- case HKQuantityTypeIdentifier.heartRate.rawValue,
301
- HKQuantityTypeIdentifier.bodyMass.rawValue:
302
- return .discreteAverage
303
- default:
329
+ switch dataType.aggregationStyle {
330
+ case .cumulative:
304
331
  return .cumulativeSum
332
+
333
+ // Newer discrete aggregation styles (iOS 15 +)
334
+ case .discreteAverage:
335
+ return .discreteAverage
336
+ @available(iOS 17.0, *)
337
+ case .discreteTemporallyWeighted:
338
+ return .discreteAverage
339
+ @available(iOS 17.0, *)
340
+ case .discreteEquivalentContinuousLevel:
341
+ return .discreteAverage
342
+ @available(iOS 17.0, *)
343
+ case .discreteArithmetic:
344
+ return .discreteAverage
345
+
346
+ // Legacy discrete fallback
347
+ case .discrete:
348
+ return .discreteAverage
349
+
350
+ @unknown default:
351
+ return .discreteAverage
305
352
  }
306
353
  }()
307
354
 
@@ -312,46 +359,68 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
312
359
  anchorDate: startDate,
313
360
  intervalComponents: interval
314
361
  )
315
-
362
+
316
363
  query.initialResultsHandler = { query, result, error in
317
364
  if let error = error {
318
365
  call.reject("Error fetching aggregated data: \(error.localizedDescription)")
319
366
  return
320
367
  }
321
-
368
+
322
369
  var aggregatedSamples: [[String: Any]] = []
323
-
324
- result?.enumerateStatistics(from: startDate, to: endDate) { statistics, stop in
325
- if let sum = statistics.sumQuantity() {
326
- let startDate = statistics.startDate.timeIntervalSince1970 * 1000
327
- let endDate = statistics.endDate.timeIntervalSince1970 * 1000
328
-
329
- var value: Double = -1.0
330
- if(dataTypeString == "steps" && dataType.is(compatibleWith: HKUnit.count())) {
331
- value = sum.doubleValue(for: HKUnit.count())
332
- } else if(dataTypeString == "active-calories" && dataType.is(compatibleWith: HKUnit.kilocalorie())) {
333
- value = sum.doubleValue(for: HKUnit.kilocalorie())
334
- } else if(dataTypeString == "mindfulness" && dataType.is(compatibleWith: HKUnit.second())) {
335
- value = sum.doubleValue(for: HKUnit.second())
370
+
371
+ result?.enumerateStatistics(from: startDate, to: endDate) { statistics, _ in
372
+ // Choose sum or average based on the options we picked
373
+ let quantity: HKQuantity? = options.contains(.cumulativeSum)
374
+ ? statistics.sumQuantity()
375
+ : statistics.averageQuantity()
376
+
377
+ guard let quantity = quantity else { return }
378
+
379
+ // Time‑bounds of this bucket
380
+ let bucketStart = statistics.startDate.timeIntervalSince1970 * 1000
381
+ let bucketEnd = statistics.endDate.timeIntervalSince1970 * 1000
382
+
383
+ // Map dataType → correct HKUnit
384
+ let unit: HKUnit = {
385
+ switch dataTypeString {
386
+ case "steps":
387
+ return .count()
388
+ case "active-calories", "total-calories":
389
+ return .kilocalorie()
390
+ case "distance":
391
+ return .meter()
392
+ case "weight":
393
+ return .gramUnit(with: .kilo)
394
+ case "height":
395
+ return .meter()
396
+ case "heart-rate":
397
+ return HKUnit.count().unitDivided(by: HKUnit.minute())
398
+ case "hrv":
399
+ return HKUnit.secondUnit(with: .milli)
400
+ case "mindfulness":
401
+ return HKUnit.second()
402
+ default:
403
+ return .count()
336
404
  }
337
-
338
-
339
- aggregatedSamples.append([
340
- "startDate": startDate,
341
- "endDate": endDate,
342
- "value": value
343
- ])
344
- }
405
+ }()
406
+
407
+ let value = quantity.doubleValue(for: unit)
408
+
409
+ aggregatedSamples.append([
410
+ "startDate": bucketStart,
411
+ "endDate": bucketEnd,
412
+ "value": value
413
+ ])
345
414
  }
346
-
415
+
347
416
  call.resolve(["aggregatedData": aggregatedSamples])
348
417
  }
349
-
418
+
350
419
  healthStore.execute(query)
351
420
  }
352
421
  }
353
422
 
354
- func queryMindfulnessAggregated(startDate: Date, endDate: Date, completion: @escaping ([[String: Any]]?, Error?) -> Void) {
423
+ func queryMindfulnessAggregated(startDate: Date, endDate: Date, completion: @escaping @Sendable ([[String: Any]]?, Error?) -> Void) {
355
424
  guard let mindfulType = HKObjectType.categoryType(forIdentifier: .mindfulSession) else {
356
425
  completion(nil, NSError(domain: "HealthKit", code: -1, userInfo: [NSLocalizedDescriptionKey: "MindfulSession type unavailable"]))
357
426
  return
@@ -399,7 +468,7 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
399
468
 
400
469
 
401
470
 
402
- private func queryAggregated(for startDate: Date, for endDate: Date, for dataType: HKQuantityType?, completion: @escaping(Double?) -> Void) {
471
+ private func queryAggregated(for startDate: Date, for endDate: Date, for dataType: HKQuantityType?, completion: @escaping @Sendable(Double?) -> Void) {
403
472
 
404
473
 
405
474
  guard let quantityType = dataType else {
@@ -553,7 +622,7 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
553
622
 
554
623
 
555
624
  // MARK: - Query Heart Rate Data
556
- private func queryHeartRate(for workout: HKWorkout, completion: @escaping ([[String: Any]], String?) -> Void) {
625
+ private func queryHeartRate(for workout: HKWorkout, completion: @escaping @Sendable ([[String: Any]], String?) -> Void) {
557
626
  let heartRateType = HKObjectType.quantityType(forIdentifier: .heartRate)!
558
627
  let predicate = HKQuery.predicateForSamples(withStart: workout.startDate, end: workout.endDate, options: .strictStartDate)
559
628
 
@@ -584,7 +653,7 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
584
653
  }
585
654
 
586
655
  // MARK: - Query Route Data
587
- private func queryRoute(for workout: HKWorkout, completion: @escaping ([[String: Any]], String?) -> Void) {
656
+ private func queryRoute(for workout: HKWorkout, completion: @escaping @Sendable ([[String: Any]], String?) -> Void) {
588
657
  let routeType = HKSeriesType.workoutRoute()
589
658
  let predicate = HKQuery.predicateForObjects(from: workout)
590
659
 
@@ -615,30 +684,33 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
615
684
  }
616
685
 
617
686
  // MARK: - Query Route Locations
618
- private func queryLocations(for route: HKWorkoutRoute, completion: @escaping ([[String: Any]]) -> Void) {
687
+ private func queryLocations(for route: HKWorkoutRoute, completion: @escaping @Sendable ([[String: Any]]) -> Void) {
619
688
  var routeLocations: [[String: Any]] = []
620
-
621
- let locationQuery = HKWorkoutRouteQuery(route: route) { query, locations, done, error in
689
+
690
+ let locationQuery = HKWorkoutRouteQuery(route: route) { _, locations, done, error in
622
691
  guard let locations = locations, error == nil else {
623
692
  completion([])
624
693
  return
625
694
  }
626
-
627
- for location in locations {
628
- let locationDict: [String: Any] = [
629
- "timestamp": location.timestamp,
630
- "lat": location.coordinate.latitude,
631
- "lng": location.coordinate.longitude,
632
- "alt": location.altitude
633
- ]
634
- routeLocations.append(locationDict)
635
- }
636
-
637
- if done {
638
- completion(routeLocations)
695
+
696
+ // Append on a dedicated serial queue so we’re race‑free without NSLock
697
+ self.routeSyncQueue.async {
698
+ for location in locations {
699
+ let locationDict: [String: Any] = [
700
+ "timestamp": location.timestamp,
701
+ "lat": location.coordinate.latitude,
702
+ "lng": location.coordinate.longitude,
703
+ "alt": location.altitude
704
+ ]
705
+ routeLocations.append(locationDict)
706
+ }
707
+
708
+ if done {
709
+ completion(routeLocations)
710
+ }
639
711
  }
640
712
  }
641
-
713
+
642
714
  healthStore.execute(locationQuery)
643
715
  }
644
716
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flomentumsolutions/capacitor-health-extended",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "Capacitor plugin for Apple HealthKit and Google Health Connect Platform",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",
@@ -19,7 +19,7 @@
19
19
  "license": "MIT",
20
20
  "repository": {
21
21
  "type": "git",
22
- "url": "git+https://github.com/Flomentum-Solutions/capacitor-health-extended.git"
22
+ "url": "https://github.com/Flomentum-Solutions/capacitor-health-extended.git"
23
23
  },
24
24
  "bugs": {
25
25
  "url": "https://github.com/Flomentum-Solutions/capacitor-health-extended/issues"
package/Package.swift DELETED
@@ -1,28 +0,0 @@
1
- // swift-tools-version: 5.9
2
- import PackageDescription
3
-
4
- let package = Package(
5
- name: "CapacitorHealthExtended",
6
- platforms: [.iOS(.v13)],
7
- products: [
8
- .library(
9
- name: "CapacitorHealthExtended",
10
- targets: ["HealthPluginPlugin"])
11
- ],
12
- dependencies: [
13
- .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", branch: "main")
14
- ],
15
- targets: [
16
- .target(
17
- name: "HealthPluginPlugin",
18
- dependencies: [
19
- .product(name: "Capacitor", package: "capacitor-swift-pm"),
20
- .product(name: "Cordova", package: "capacitor-swift-pm")
21
- ],
22
- path: "ios/Sources/HealthPluginPlugin"),
23
- .testTarget(
24
- name: "HealthPluginPluginTests",
25
- dependencies: ["HealthPluginPlugin"],
26
- path: "ios/Tests/HealthPluginPluginTests")
27
- ]
28
- )