@flomentumsolutions/capacitor-health-extended 0.0.5 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Package.swift CHANGED
@@ -2,11 +2,11 @@
2
2
  import PackageDescription
3
3
 
4
4
  let package = Package(
5
- name: "CapacitorHealth",
5
+ name: "CapacitorHealthExtended",
6
6
  platforms: [.iOS(.v13)],
7
7
  products: [
8
8
  .library(
9
- name: "CapacitorHealth",
9
+ name: "CapacitorHealthExtended",
10
10
  targets: ["HealthPluginPlugin"])
11
11
  ],
12
12
  dependencies: [
@@ -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? {
@@ -54,7 +55,9 @@ enum class CapHealthPermission {
54
55
  Permission(alias = "READ_DISTANCE", strings = ["android.permission.health.READ_DISTANCE"]),
55
56
  Permission(alias = "READ_ACTIVE_CALORIES", strings = ["android.permission.health.READ_ACTIVE_CALORIES_BURNED"]),
56
57
  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"])
58
+ Permission(alias = "READ_HEART_RATE", strings = ["android.permission.health.READ_HEART_RATE"]),
59
+ Permission(alias = "READ_HRV", strings = ["android.permission.health.READ_HEART_RATE_VARIABILITY"]),
60
+ Permission(alias = "READ_BLOOD_PRESSURE", strings = ["android.permission.health.READ_BLOOD_PRESSURE"])
58
61
  ]
59
62
  )
60
63
 
@@ -75,7 +78,9 @@ class HealthPlugin : Plugin() {
75
78
  CapHealthPermission.READ_ACTIVE_CALORIES to HealthPermission.getReadPermission(ActiveCaloriesBurnedRecord::class),
76
79
  CapHealthPermission.READ_TOTAL_CALORIES to HealthPermission.getReadPermission(TotalCaloriesBurnedRecord::class),
77
80
  CapHealthPermission.READ_DISTANCE to HealthPermission.getReadPermission(DistanceRecord::class),
78
- CapHealthPermission.READ_WORKOUTS to HealthPermission.getReadPermission(ExerciseSessionRecord::class)
81
+ CapHealthPermission.READ_WORKOUTS to HealthPermission.getReadPermission(ExerciseSessionRecord::class),
82
+ CapHealthPermission.READ_HRV to HealthPermission.getReadPermission(HeartRateVariabilitySdnnRecord::class),
83
+ CapHealthPermission.READ_BLOOD_PRESSURE to HealthPermission.getReadPermission(BloodPressureRecord::class)
79
84
  )
80
85
 
81
86
  override fun load() {
@@ -258,6 +263,7 @@ class HealthPlugin : Plugin() {
258
263
  TotalCaloriesBurnedRecord.ENERGY_TOTAL
259
264
  ) { it?.inKilocalories }
260
265
  "distance" -> metricAndMapper("distance", CapHealthPermission.READ_DISTANCE, DistanceRecord.DISTANCE_TOTAL) { it?.inMeters }
266
+ "hrv" -> metricAndMapper("hrv", CapHealthPermission.READ_HRV, HeartRateVariabilitySdnnRecord.SDNN_AVG) { it }
261
267
  else -> throw RuntimeException("Unsupported dataType: $dataType")
262
268
  }
263
269
  }
@@ -277,6 +283,8 @@ class HealthPlugin : Plugin() {
277
283
  "heart-rate" -> readLatestHeartRate()
278
284
  "weight" -> readLatestWeight()
279
285
  "steps" -> readLatestSteps()
286
+ "hrv" -> readLatestHrv()
287
+ "blood-pressure" -> readLatestBloodPressure()
280
288
  else -> {
281
289
  call.reject("Unsupported data type: $dataType")
282
290
  return@launch
@@ -284,7 +292,7 @@ class HealthPlugin : Plugin() {
284
292
  }
285
293
  call.resolve(result)
286
294
  } catch (e: Exception) {
287
- Log.e(tag, "queryLatestSample: Error fetching latest heart-rate", e)
295
+ Log.e(tag, "queryLatestSample: Error fetching latest $dataType", e)
288
296
  call.reject("Error fetching latest $dataType: ${e.message}")
289
297
  }
290
298
  }
@@ -344,6 +352,41 @@ class HealthPlugin : Plugin() {
344
352
  }
345
353
  }
346
354
 
355
+ private suspend fun readLatestHrv(): JSObject {
356
+ if (!hasPermission(CapHealthPermission.READ_HRV)) {
357
+ throw Exception("Permission for HRV not granted")
358
+ }
359
+ val request = ReadRecordsRequest(
360
+ recordType = HeartRateVariabilitySdnnRecord::class,
361
+ timeRangeFilter = TimeRangeFilter.after(Instant.EPOCH),
362
+ pageSize = 1
363
+ )
364
+ val result = healthConnectClient.readRecords(request)
365
+ val record = result.records.firstOrNull() ?: throw Exception("No HRV data found")
366
+ return JSObject().apply {
367
+ put("timestamp", record.time.toString())
368
+ put("value", record.sdnnMillis)
369
+ }
370
+ }
371
+
372
+ private suspend fun readLatestBloodPressure(): JSObject {
373
+ if (!hasPermission(CapHealthPermission.READ_BLOOD_PRESSURE)) {
374
+ throw Exception("Permission for blood pressure not granted")
375
+ }
376
+ val request = ReadRecordsRequest(
377
+ recordType = BloodPressureRecord::class,
378
+ timeRangeFilter = TimeRangeFilter.after(Instant.EPOCH),
379
+ pageSize = 1
380
+ )
381
+ val result = healthConnectClient.readRecords(request)
382
+ val record = result.records.firstOrNull() ?: throw Exception("No blood pressure data found")
383
+ return JSObject().apply {
384
+ put("timestamp", record.time.toString())
385
+ put("systolic", record.systolic.inMillimetersOfMercury)
386
+ put("diastolic", record.diastolic.inMillimetersOfMercury)
387
+ }
388
+ }
389
+
347
390
  @PluginMethod
348
391
  fun queryAggregated(call: PluginCall) {
349
392
  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_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_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,10 @@ 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 "blood-pressure":
152
+ return nil // handled above
102
153
  default:
103
154
  return nil
104
155
  }
@@ -130,6 +181,8 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
130
181
  unit = HKUnit.count().unitDivided(by: HKUnit.minute())
131
182
  } else if dataTypeString == "weight" {
132
183
  unit = .gramUnit(with: .kilo)
184
+ } else if dataTypeString == "hrv" {
185
+ unit = HKUnit.secondUnit(with: .milli)
133
186
  }
134
187
 
135
188
  let value = quantitySample.quantity.doubleValue(for: unit)
@@ -180,6 +233,13 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
180
233
  ].compactMap{$0}
181
234
  case "READ_MINDFULNESS":
182
235
  return [HKObjectType.categoryType(forIdentifier: .mindfulSession)!].compactMap{$0}
236
+ case "READ_HRV":
237
+ return [HKObjectType.quantityType(forIdentifier: .heartRateVariabilitySDNN)].compactMap { $0 }
238
+ case "READ_BLOOD_PRESSURE":
239
+ return [
240
+ HKObjectType.quantityType(forIdentifier: .bloodPressureSystolic),
241
+ HKObjectType.quantityType(forIdentifier: .bloodPressureDiastolic)
242
+ ].compactMap { $0 }
183
243
  default:
184
244
  return []
185
245
  }
@@ -195,6 +255,8 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
195
255
  return HKObjectType.quantityType(forIdentifier: .heartRate)
196
256
  case "weight":
197
257
  return HKObjectType.quantityType(forIdentifier: .bodyMass)
258
+ case "hrv":
259
+ return HKObjectType.quantityType(forIdentifier: .heartRateVariabilitySDNN)
198
260
  default:
199
261
  return nil
200
262
  }
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.6",
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",