@flomentumsolutions/capacitor-health-extended 0.0.5 → 0.0.7

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>
@@ -32,7 +32,8 @@ import java.util.concurrent.atomic.AtomicReference
32
32
  import androidx.core.net.toUri
33
33
 
34
34
  enum class CapHealthPermission {
35
- READ_STEPS, READ_WORKOUTS, READ_HEART_RATE, READ_ACTIVE_CALORIES, READ_TOTAL_CALORIES, READ_DISTANCE, READ_WEIGHT;
35
+ READ_STEPS, READ_WORKOUTS, READ_HEART_RATE, READ_ACTIVE_CALORIES, READ_TOTAL_CALORIES, READ_DISTANCE, READ_WEIGHT
36
+ , READ_HRV, READ_BLOOD_PRESSURE;
36
37
 
37
38
  companion object {
38
39
  fun from(s: String): CapHealthPermission? {
@@ -50,11 +51,14 @@ enum class CapHealthPermission {
50
51
  permissions = [
51
52
  Permission(alias = "READ_STEPS", strings = ["android.permission.health.READ_STEPS"]),
52
53
  Permission(alias = "READ_WEIGHT", strings = ["android.permission.health.READ_WEIGHT"]),
54
+ Permission(alias = "READ_HEIGHT", strings = ["android.permission.health.READ_HEIGHT"]),
53
55
  Permission(alias = "READ_WORKOUTS", strings = ["android.permission.health.READ_EXERCISE"]),
54
56
  Permission(alias = "READ_DISTANCE", strings = ["android.permission.health.READ_DISTANCE"]),
55
57
  Permission(alias = "READ_ACTIVE_CALORIES", strings = ["android.permission.health.READ_ACTIVE_CALORIES_BURNED"]),
56
58
  Permission(alias = "READ_TOTAL_CALORIES", strings = ["android.permission.health.READ_TOTAL_CALORIES_BURNED"]),
57
- Permission(alias = "READ_HEART_RATE", strings = ["android.permission.health.READ_HEART_RATE"])
59
+ Permission(alias = "READ_HEART_RATE", strings = ["android.permission.health.READ_HEART_RATE"]),
60
+ Permission(alias = "READ_HRV", strings = ["android.permission.health.READ_HEART_RATE_VARIABILITY"]),
61
+ Permission(alias = "READ_BLOOD_PRESSURE", strings = ["android.permission.health.READ_BLOOD_PRESSURE"])
58
62
  ]
59
63
  )
60
64
 
@@ -75,7 +79,9 @@ class HealthPlugin : Plugin() {
75
79
  CapHealthPermission.READ_ACTIVE_CALORIES to HealthPermission.getReadPermission(ActiveCaloriesBurnedRecord::class),
76
80
  CapHealthPermission.READ_TOTAL_CALORIES to HealthPermission.getReadPermission(TotalCaloriesBurnedRecord::class),
77
81
  CapHealthPermission.READ_DISTANCE to HealthPermission.getReadPermission(DistanceRecord::class),
78
- CapHealthPermission.READ_WORKOUTS to HealthPermission.getReadPermission(ExerciseSessionRecord::class)
82
+ CapHealthPermission.READ_WORKOUTS to HealthPermission.getReadPermission(ExerciseSessionRecord::class),
83
+ CapHealthPermission.READ_HRV to HealthPermission.getReadPermission(HeartRateVariabilitySdnnRecord::class),
84
+ CapHealthPermission.READ_BLOOD_PRESSURE to HealthPermission.getReadPermission(BloodPressureRecord::class)
79
85
  )
80
86
 
81
87
  override fun load() {
@@ -258,6 +264,7 @@ class HealthPlugin : Plugin() {
258
264
  TotalCaloriesBurnedRecord.ENERGY_TOTAL
259
265
  ) { it?.inKilocalories }
260
266
  "distance" -> metricAndMapper("distance", CapHealthPermission.READ_DISTANCE, DistanceRecord.DISTANCE_TOTAL) { it?.inMeters }
267
+ "hrv" -> metricAndMapper("hrv", CapHealthPermission.READ_HRV, HeartRateVariabilitySdnnRecord.SDNN_AVG) { it }
261
268
  else -> throw RuntimeException("Unsupported dataType: $dataType")
262
269
  }
263
270
  }
@@ -277,6 +284,8 @@ class HealthPlugin : Plugin() {
277
284
  "heart-rate" -> readLatestHeartRate()
278
285
  "weight" -> readLatestWeight()
279
286
  "steps" -> readLatestSteps()
287
+ "hrv" -> readLatestHrv()
288
+ "blood-pressure" -> readLatestBloodPressure()
280
289
  else -> {
281
290
  call.reject("Unsupported data type: $dataType")
282
291
  return@launch
@@ -284,7 +293,7 @@ class HealthPlugin : Plugin() {
284
293
  }
285
294
  call.resolve(result)
286
295
  } catch (e: Exception) {
287
- Log.e(tag, "queryLatestSample: Error fetching latest heart-rate", e)
296
+ Log.e(tag, "queryLatestSample: Error fetching latest $dataType", e)
288
297
  call.reject("Error fetching latest $dataType: ${e.message}")
289
298
  }
290
299
  }
@@ -344,6 +353,41 @@ class HealthPlugin : Plugin() {
344
353
  }
345
354
  }
346
355
 
356
+ private suspend fun readLatestHrv(): JSObject {
357
+ if (!hasPermission(CapHealthPermission.READ_HRV)) {
358
+ throw Exception("Permission for HRV not granted")
359
+ }
360
+ val request = ReadRecordsRequest(
361
+ recordType = HeartRateVariabilitySdnnRecord::class,
362
+ timeRangeFilter = TimeRangeFilter.after(Instant.EPOCH),
363
+ pageSize = 1
364
+ )
365
+ val result = healthConnectClient.readRecords(request)
366
+ val record = result.records.firstOrNull() ?: throw Exception("No HRV data found")
367
+ return JSObject().apply {
368
+ put("timestamp", record.time.toString())
369
+ put("value", record.sdnnMillis)
370
+ }
371
+ }
372
+
373
+ private suspend fun readLatestBloodPressure(): JSObject {
374
+ if (!hasPermission(CapHealthPermission.READ_BLOOD_PRESSURE)) {
375
+ throw Exception("Permission for blood pressure not granted")
376
+ }
377
+ val request = ReadRecordsRequest(
378
+ recordType = BloodPressureRecord::class,
379
+ timeRangeFilter = TimeRangeFilter.after(Instant.EPOCH),
380
+ pageSize = 1
381
+ )
382
+ val result = healthConnectClient.readRecords(request)
383
+ val record = result.records.firstOrNull() ?: throw Exception("No blood pressure data found")
384
+ return JSObject().apply {
385
+ put("timestamp", record.time.toString())
386
+ put("systolic", record.systolic.inMillimetersOfMercury)
387
+ put("diastolic", record.diastolic.inMillimetersOfMercury)
388
+ }
389
+ }
390
+
347
391
  @PluginMethod
348
392
  fun queryAggregated(call: PluginCall) {
349
393
  if (!ensureClientInitialized(call)) return
@@ -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';
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
  }
@@ -97,7 +97,7 @@ export interface Workout {
97
97
  export interface QueryAggregatedRequest {
98
98
  startDate: string;
99
99
  endDate: string;
100
- dataType: 'steps' | 'active-calories' | 'mindfulness';
100
+ dataType: 'steps' | 'active-calories' | 'mindfulness' | 'hrv' | 'blood-pressure';
101
101
  bucket: string;
102
102
  }
103
103
  export interface QueryAggregatedResponse {
@@ -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\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';\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"]}
package/dist/plugin.js CHANGED
@@ -1,4 +1,4 @@
1
- var capacitorHealthPlugin = (function (exports, core) {
1
+ var capacitorHealthExtendedPlugin = (function (exports, core) {
2
2
  'use strict';
3
3
 
4
4
  const Health = core.registerPlugin('HealthPlugin', {});
@@ -86,6 +86,53 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
86
86
  call.reject("Missing data type")
87
87
  return
88
88
  }
89
+ // ---- Special handling for blood‑pressure correlation ----
90
+ if dataTypeString == "blood-pressure" {
91
+ guard let bpType = HKObjectType.correlationType(forIdentifier: .bloodPressure) else {
92
+ call.reject("Blood pressure type not available")
93
+ return
94
+ }
95
+
96
+ let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false)
97
+ let predicate = HKQuery.predicateForSamples(withStart: Date.distantPast, end: Date(), options: .strictEndDate)
98
+
99
+ let query = HKSampleQuery(sampleType: bpType, predicate: predicate, limit: 1, sortDescriptors: [sortDescriptor]) { _, samples, error in
100
+
101
+ guard let bpCorrelation = samples?.first as? HKCorrelation else {
102
+ if let error = error {
103
+ call.reject("Error fetching latest blood pressure sample", "NO_SAMPLE", error)
104
+ } else {
105
+ call.reject("No blood pressure sample found", "NO_SAMPLE")
106
+ }
107
+ return
108
+ }
109
+
110
+ let unit = HKUnit.millimeterOfMercury()
111
+
112
+ let systolicSamples = bpCorrelation.objects(for: HKObjectType.quantityType(forIdentifier: .bloodPressureSystolic)!)
113
+ let diastolicSamples = bpCorrelation.objects(for: HKObjectType.quantityType(forIdentifier: .bloodPressureDiastolic)!)
114
+
115
+ guard let systolicSample = systolicSamples.first as? HKQuantitySample,
116
+ let diastolicSample = diastolicSamples.first as? HKQuantitySample else {
117
+ call.reject("Incomplete blood pressure data", "NO_SAMPLE")
118
+ return
119
+ }
120
+
121
+ let systolicValue = systolicSample.quantity.doubleValue(for: unit)
122
+ let diastolicValue = diastolicSample.quantity.doubleValue(for: unit)
123
+ let timestamp = bpCorrelation.startDate.timeIntervalSince1970 * 1000
124
+
125
+ call.resolve([
126
+ "systolic": systolicValue,
127
+ "diastolic": diastolicValue,
128
+ "timestamp": timestamp,
129
+ "unit": unit.unitString
130
+ ])
131
+ }
132
+
133
+ healthStore.execute(query)
134
+ return
135
+ }
89
136
  guard aggregateTypeToHKQuantityType(dataTypeString) != nil else {
90
137
  call.reject("Invalid data type")
91
138
  return
@@ -99,6 +146,18 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
99
146
  return HKObjectType.quantityType(forIdentifier: .bodyMass)
100
147
  case "steps":
101
148
  return HKObjectType.quantityType(forIdentifier: .stepCount)
149
+ case "hrv":
150
+ return HKObjectType.quantityType(forIdentifier: .heartRateVariabilitySDNN)
151
+ case "height":
152
+ return HKObjectType.quantityType(forIdentifier: .height)
153
+ case "distance":
154
+ return HKObjectType.quantityType(forIdentifier: .distanceWalkingRunning)
155
+ case "active-calories":
156
+ return HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)
157
+ case "total-calories":
158
+ return HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)
159
+ case "blood-pressure":
160
+ return nil // handled above
102
161
  default:
103
162
  return nil
104
163
  }
@@ -130,8 +189,15 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
130
189
  unit = HKUnit.count().unitDivided(by: HKUnit.minute())
131
190
  } else if dataTypeString == "weight" {
132
191
  unit = .gramUnit(with: .kilo)
192
+ } else if dataTypeString == "hrv" {
193
+ unit = HKUnit.secondUnit(with: .milli)
194
+ } else if dataTypeString == "distance" {
195
+ unit = HKUnit.meter()
196
+ } else if dataTypeString == "active-calories" || dataTypeString == "total-calories" {
197
+ unit = HKUnit.kilocalorie()
198
+ } else if dataTypeString == "height" {
199
+ unit = HKUnit.meter()
133
200
  }
134
-
135
201
  let value = quantitySample.quantity.doubleValue(for: unit)
136
202
  let timestamp = quantitySample.startDate.timeIntervalSince1970 * 1000
137
203
 
@@ -163,6 +229,13 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
163
229
  return [HKObjectType.quantityType(forIdentifier: .stepCount)].compactMap{$0}
164
230
  case "READ_WEIGHT":
165
231
  return [HKObjectType.quantityType(forIdentifier: .bodyMass)].compactMap{$0}
232
+ case "READ_HEIGHT":
233
+ return [HKObjectType.quantityType(forIdentifier: .height)].compactMap { $0 }
234
+ case "READ_TOTAL_CALORIES":
235
+ return [
236
+ HKObjectType.quantityType(forIdentifier: .activeEnergyBurned),
237
+ HKObjectType.quantityType(forIdentifier: .basalEnergyBurned) // iOS 16+
238
+ ].compactMap { $0 }
166
239
  case "READ_ACTIVE_CALORIES":
167
240
  return [HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)].compactMap{$0}
168
241
  case "READ_WORKOUTS":
@@ -180,6 +253,13 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
180
253
  ].compactMap{$0}
181
254
  case "READ_MINDFULNESS":
182
255
  return [HKObjectType.categoryType(forIdentifier: .mindfulSession)!].compactMap{$0}
256
+ case "READ_HRV":
257
+ return [HKObjectType.quantityType(forIdentifier: .heartRateVariabilitySDNN)].compactMap { $0 }
258
+ case "READ_BLOOD_PRESSURE":
259
+ return [
260
+ HKObjectType.quantityType(forIdentifier: .bloodPressureSystolic),
261
+ HKObjectType.quantityType(forIdentifier: .bloodPressureDiastolic)
262
+ ].compactMap { $0 }
183
263
  default:
184
264
  return []
185
265
  }
@@ -195,6 +275,14 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
195
275
  return HKObjectType.quantityType(forIdentifier: .heartRate)
196
276
  case "weight":
197
277
  return HKObjectType.quantityType(forIdentifier: .bodyMass)
278
+ case "hrv":
279
+ return HKObjectType.quantityType(forIdentifier: .heartRateVariabilitySDNN)
280
+ case "distance":
281
+ return HKObjectType.quantityType(forIdentifier: .distanceWalkingRunning) // pick one rep type
282
+ case "total-calories":
283
+ return HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)
284
+ case "height":
285
+ return HKObjectType.quantityType(forIdentifier: .height)
198
286
  default:
199
287
  return nil
200
288
  }
@@ -234,11 +322,12 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
234
322
  }
235
323
 
236
324
  let options: HKStatisticsOptions = {
237
- switch dataType.identifier {
238
- case HKQuantityTypeIdentifier.heartRate.rawValue,
239
- HKQuantityTypeIdentifier.bodyMass.rawValue:
240
- return .discreteAverage
241
- default:
325
+ switch dataType.aggregationStyle {
326
+ case .cumulative:
327
+ return .cumulativeSum
328
+ case .discrete:
329
+ return .discreteAverage // or .discreteMin / Max when needed
330
+ @unknown default:
242
331
  return .cumulativeSum
243
332
  }
244
333
  }()
@@ -250,41 +339,63 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
250
339
  anchorDate: startDate,
251
340
  intervalComponents: interval
252
341
  )
253
-
342
+
254
343
  query.initialResultsHandler = { query, result, error in
255
344
  if let error = error {
256
345
  call.reject("Error fetching aggregated data: \(error.localizedDescription)")
257
346
  return
258
347
  }
259
-
348
+
260
349
  var aggregatedSamples: [[String: Any]] = []
261
-
262
- result?.enumerateStatistics(from: startDate, to: endDate) { statistics, stop in
263
- if let sum = statistics.sumQuantity() {
264
- let startDate = statistics.startDate.timeIntervalSince1970 * 1000
265
- let endDate = statistics.endDate.timeIntervalSince1970 * 1000
266
-
267
- var value: Double = -1.0
268
- if(dataTypeString == "steps" && dataType.is(compatibleWith: HKUnit.count())) {
269
- value = sum.doubleValue(for: HKUnit.count())
270
- } else if(dataTypeString == "active-calories" && dataType.is(compatibleWith: HKUnit.kilocalorie())) {
271
- value = sum.doubleValue(for: HKUnit.kilocalorie())
272
- } else if(dataTypeString == "mindfulness" && dataType.is(compatibleWith: HKUnit.second())) {
273
- value = sum.doubleValue(for: HKUnit.second())
350
+
351
+ result?.enumerateStatistics(from: startDate, to: endDate) { statistics, _ in
352
+ // Choose sum or average based on the options we picked
353
+ let quantity: HKQuantity? = options.contains(.cumulativeSum)
354
+ ? statistics.sumQuantity()
355
+ : statistics.averageQuantity()
356
+
357
+ guard let quantity = quantity else { return }
358
+
359
+ // Time‑bounds of this bucket
360
+ let bucketStart = statistics.startDate.timeIntervalSince1970 * 1000
361
+ let bucketEnd = statistics.endDate.timeIntervalSince1970 * 1000
362
+
363
+ // Map dataType → correct HKUnit
364
+ let unit: HKUnit = {
365
+ switch dataTypeString {
366
+ case "steps":
367
+ return .count()
368
+ case "active-calories", "total-calories":
369
+ return .kilocalorie()
370
+ case "distance":
371
+ return .meter()
372
+ case "weight":
373
+ return .gramUnit(with: .kilo)
374
+ case "height":
375
+ return .meter()
376
+ case "heart-rate":
377
+ return HKUnit.count().unitDivided(by: HKUnit.minute())
378
+ case "hrv":
379
+ return HKUnit.secondUnit(with: .milli)
380
+ case "mindfulness":
381
+ return HKUnit.second()
382
+ default:
383
+ return .count()
274
384
  }
275
-
276
-
277
- aggregatedSamples.append([
278
- "startDate": startDate,
279
- "endDate": endDate,
280
- "value": value
281
- ])
282
- }
385
+ }()
386
+
387
+ let value = quantity.doubleValue(for: unit)
388
+
389
+ aggregatedSamples.append([
390
+ "startDate": bucketStart,
391
+ "endDate": bucketEnd,
392
+ "value": value
393
+ ])
283
394
  }
284
-
395
+
285
396
  call.resolve(["aggregatedData": aggregatedSamples])
286
397
  }
287
-
398
+
288
399
  healthStore.execute(query)
289
400
  }
290
401
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flomentumsolutions/capacitor-health-extended",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
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: "CapacitorHealth",
6
- platforms: [.iOS(.v13)],
7
- products: [
8
- .library(
9
- name: "CapacitorHealth",
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
- )