@capgo/capacitor-health 8.2.17 → 8.2.18

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/README.md CHANGED
@@ -174,10 +174,45 @@ await Health.saveSample({
174
174
  | `oxygenSaturation` | `percent` | Blood oxygen saturation (SpO2) |
175
175
  | `restingHeartRate` | `bpm` | Resting heart rate |
176
176
  | `heartRateVariability` | `millisecond` | Heart rate variability (HRV) |
177
+ | `bloodPressure` | `mmHg` | Blood pressure (requires systolic/diastolic values) |
178
+ | `bloodGlucose` | `mg/dL` | Blood glucose level |
179
+ | `bodyTemperature` | `celsius` | Body temperature |
180
+ | `height` | `centimeter` | Body height |
181
+ | `flightsClimbed` | `count` | Floors / flights of stairs climbed |
182
+ | `exerciseTime` | `minute` | Apple Exercise Time (iOS only) |
183
+ | `distanceCycling` | `meter` | Cycling distance |
184
+ | `bodyFat` | `percent` | Body fat percentage |
185
+ | `basalBodyTemperature` | `celsius` | Basal body temperature |
186
+ | `basalCalories` | `kilocalorie` | Basal metabolic rate / resting energy |
187
+ | `totalCalories` | `kilocalorie` | Total energy burned (active + basal) |
188
+ | `mindfulness` | `minute` | Mindfulness / meditation sessions |
177
189
  | `workouts` | N/A | Workout sessions (read-only, use with `queryWorkouts()`) |
178
190
 
179
191
  All write operations expect the default unit shown above. On Android the `metadata` option is currently ignored by Health Connect.
180
192
 
193
+ **Blood Pressure:** Blood pressure requires both systolic and diastolic values:
194
+
195
+ ```ts
196
+ await Health.saveSample({
197
+ dataType: 'bloodPressure',
198
+ value: 120, // systolic value (used as main value)
199
+ systolic: 120,
200
+ diastolic: 80,
201
+ startDate: new Date().toISOString(),
202
+ });
203
+
204
+ // Reading blood pressure returns samples with systolic/diastolic fields
205
+ const { samples } = await Health.readSamples({
206
+ dataType: 'bloodPressure',
207
+ startDate: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(),
208
+ endDate: new Date().toISOString(),
209
+ });
210
+
211
+ samples.forEach((sample) => {
212
+ console.log(`BP: ${sample.systolic}/${sample.diastolic} mmHg`);
213
+ });
214
+ ```
215
+
181
216
  **Note about workouts:** To query workout data using `queryWorkouts()`, you need to explicitly request `workouts` permission:
182
217
 
183
218
  ```ts
@@ -521,6 +556,8 @@ Supported on iOS (HealthKit) and Android (Health Connect).
521
556
  | **`sourceName`** | <code>string</code> | |
522
557
  | **`sourceId`** | <code>string</code> | |
523
558
  | **`sleepState`** | <code><a href="#sleepstate">SleepState</a></code> | For sleep data, indicates the sleep state (e.g., 'asleep', 'awake', 'rem', 'deep', 'light'). |
559
+ | **`systolic`** | <code>number</code> | For blood pressure data, the systolic value in mmHg. |
560
+ | **`diastolic`** | <code>number</code> | For blood pressure data, the diastolic value in mmHg. |
524
561
 
525
562
 
526
563
  #### QueryOptions
@@ -544,6 +581,8 @@ Supported on iOS (HealthKit) and Android (Health Connect).
544
581
  | **`startDate`** | <code>string</code> | ISO 8601 start date for the sample. Defaults to now. |
545
582
  | **`endDate`** | <code>string</code> | ISO 8601 end date for the sample. Defaults to startDate. |
546
583
  | **`metadata`** | <code><a href="#record">Record</a>&lt;string, string&gt;</code> | Metadata key-value pairs forwarded to the native APIs where supported. |
584
+ | **`systolic`** | <code>number</code> | For blood pressure data, the systolic value in mmHg. Required when dataType is 'bloodPressure'. |
585
+ | **`diastolic`** | <code>number</code> | For blood pressure data, the diastolic value in mmHg. Required when dataType is 'bloodPressure'. |
547
586
 
548
587
 
549
588
  #### QueryWorkoutsResult
@@ -614,12 +653,12 @@ Supported on iOS (HealthKit) and Android (Health Connect).
614
653
 
615
654
  #### HealthDataType
616
655
 
617
- <code>'steps' | 'distance' | 'calories' | 'heartRate' | 'weight' | 'sleep' | 'respiratoryRate' | 'oxygenSaturation' | 'restingHeartRate' | 'heartRateVariability'</code>
656
+ <code>'steps' | 'distance' | 'calories' | 'heartRate' | 'weight' | 'sleep' | 'respiratoryRate' | 'oxygenSaturation' | 'restingHeartRate' | 'heartRateVariability' | 'bloodPressure' | 'bloodGlucose' | 'bodyTemperature' | 'height' | 'flightsClimbed' | 'exerciseTime' | 'distanceCycling' | 'bodyFat' | 'basalBodyTemperature' | 'basalCalories' | 'totalCalories' | 'mindfulness'</code>
618
657
 
619
658
 
620
659
  #### HealthUnit
621
660
 
622
- <code>'count' | 'meter' | 'kilocalorie' | 'bpm' | 'kilogram' | 'minute' | 'percent' | 'millisecond'</code>
661
+ <code>'count' | 'meter' | 'kilocalorie' | 'bpm' | 'kilogram' | 'minute' | 'percent' | 'millisecond' | 'mmHg' | 'mg/dL' | 'celsius' | 'fahrenheit' | 'centimeter'</code>
623
662
 
624
663
 
625
664
  #### SleepState
@@ -62,7 +62,7 @@ dependencies {
62
62
  implementation project(':capacitor-android')
63
63
  implementation "androidx.appcompat:appcompat:${androidxAppCompatVersion}"
64
64
  implementation ("org.jetbrains.kotlin:kotlin-stdlib:" + project.ext.kotlinVersion)
65
- implementation 'androidx.health.connect:connect-client:1.1.0-alpha10'
65
+ implementation 'androidx.health.connect:connect-client:1.1.0'
66
66
  implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1'
67
67
  testImplementation "junit:junit:${junitVersion}"
68
68
  androidTestImplementation "androidx.test.ext:junit:${androidxJunitVersion}"
@@ -19,6 +19,26 @@
19
19
  <uses-permission android:name="android.permission.health.WRITE_RESTING_HEART_RATE" />
20
20
  <uses-permission android:name="android.permission.health.READ_HEART_RATE_VARIABILITY" />
21
21
  <uses-permission android:name="android.permission.health.WRITE_HEART_RATE_VARIABILITY" />
22
+ <uses-permission android:name="android.permission.health.READ_BLOOD_PRESSURE" />
23
+ <uses-permission android:name="android.permission.health.WRITE_BLOOD_PRESSURE" />
24
+ <uses-permission android:name="android.permission.health.READ_BLOOD_GLUCOSE" />
25
+ <uses-permission android:name="android.permission.health.WRITE_BLOOD_GLUCOSE" />
26
+ <uses-permission android:name="android.permission.health.READ_BODY_TEMPERATURE" />
27
+ <uses-permission android:name="android.permission.health.WRITE_BODY_TEMPERATURE" />
28
+ <uses-permission android:name="android.permission.health.READ_HEIGHT" />
29
+ <uses-permission android:name="android.permission.health.WRITE_HEIGHT" />
30
+ <uses-permission android:name="android.permission.health.READ_FLOORS_CLIMBED" />
31
+ <uses-permission android:name="android.permission.health.WRITE_FLOORS_CLIMBED" />
32
+ <uses-permission android:name="android.permission.health.READ_BODY_FAT" />
33
+ <uses-permission android:name="android.permission.health.WRITE_BODY_FAT" />
34
+ <uses-permission android:name="android.permission.health.READ_BASAL_BODY_TEMPERATURE" />
35
+ <uses-permission android:name="android.permission.health.WRITE_BASAL_BODY_TEMPERATURE" />
36
+ <uses-permission android:name="android.permission.health.READ_BASAL_METABOLIC_RATE" />
37
+ <uses-permission android:name="android.permission.health.WRITE_BASAL_METABOLIC_RATE" />
38
+ <uses-permission android:name="android.permission.health.READ_TOTAL_CALORIES_BURNED" />
39
+ <uses-permission android:name="android.permission.health.WRITE_TOTAL_CALORIES_BURNED" />
40
+ <uses-permission android:name="android.permission.health.READ_MINDFULNESS" />
41
+ <uses-permission android:name="android.permission.health.WRITE_MINDFULNESS" />
22
42
  <uses-permission android:name="android.permission.health.READ_EXERCISE" />
23
43
 
24
44
  <!-- Query for Health Connect availability -->
@@ -1,19 +1,31 @@
1
1
  package app.capgo.plugin.health
2
2
 
3
+ import androidx.health.connect.client.feature.ExperimentalMindfulnessSessionApi
3
4
  import androidx.health.connect.client.permission.HealthPermission
4
5
  import androidx.health.connect.client.records.ActiveCaloriesBurnedRecord
6
+ import androidx.health.connect.client.records.BasalBodyTemperatureRecord
7
+ import androidx.health.connect.client.records.BasalMetabolicRateRecord
8
+ import androidx.health.connect.client.records.BloodGlucoseRecord
9
+ import androidx.health.connect.client.records.BloodPressureRecord
10
+ import androidx.health.connect.client.records.BodyFatRecord
11
+ import androidx.health.connect.client.records.BodyTemperatureRecord
5
12
  import androidx.health.connect.client.records.DistanceRecord
13
+ import androidx.health.connect.client.records.FloorsClimbedRecord
6
14
  import androidx.health.connect.client.records.HeartRateRecord
7
15
  import androidx.health.connect.client.records.HeartRateVariabilityRmssdRecord
16
+ import androidx.health.connect.client.records.HeightRecord
17
+ import androidx.health.connect.client.records.MindfulnessSessionRecord
8
18
  import androidx.health.connect.client.records.OxygenSaturationRecord
9
19
  import androidx.health.connect.client.records.Record
10
20
  import androidx.health.connect.client.records.RespiratoryRateRecord
11
21
  import androidx.health.connect.client.records.RestingHeartRateRecord
12
22
  import androidx.health.connect.client.records.SleepSessionRecord
13
23
  import androidx.health.connect.client.records.StepsRecord
24
+ import androidx.health.connect.client.records.TotalCaloriesBurnedRecord
14
25
  import androidx.health.connect.client.records.WeightRecord
15
26
  import kotlin.reflect.KClass
16
27
 
28
+ @OptIn(ExperimentalMindfulnessSessionApi::class)
17
29
  enum class HealthDataType(
18
30
  val identifier: String,
19
31
  val recordClass: KClass<out Record>,
@@ -28,7 +40,18 @@ enum class HealthDataType(
28
40
  RESPIRATORY_RATE("respiratoryRate", RespiratoryRateRecord::class, "bpm"),
29
41
  OXYGEN_SATURATION("oxygenSaturation", OxygenSaturationRecord::class, "percent"),
30
42
  RESTING_HEART_RATE("restingHeartRate", RestingHeartRateRecord::class, "bpm"),
31
- HEART_RATE_VARIABILITY("heartRateVariability", HeartRateVariabilityRmssdRecord::class, "millisecond");
43
+ HEART_RATE_VARIABILITY("heartRateVariability", HeartRateVariabilityRmssdRecord::class, "millisecond"),
44
+ BLOOD_PRESSURE("bloodPressure", BloodPressureRecord::class, "mmHg"),
45
+ BLOOD_GLUCOSE("bloodGlucose", BloodGlucoseRecord::class, "mg/dL"),
46
+ BODY_TEMPERATURE("bodyTemperature", BodyTemperatureRecord::class, "celsius"),
47
+ HEIGHT("height", HeightRecord::class, "centimeter"),
48
+ FLIGHTS_CLIMBED("flightsClimbed", FloorsClimbedRecord::class, "count"),
49
+ DISTANCE_CYCLING("distanceCycling", DistanceRecord::class, "meter"),
50
+ BODY_FAT("bodyFat", BodyFatRecord::class, "percent"),
51
+ BASAL_BODY_TEMPERATURE("basalBodyTemperature", BasalBodyTemperatureRecord::class, "celsius"),
52
+ BASAL_CALORIES("basalCalories", BasalMetabolicRateRecord::class, "kilocalorie"),
53
+ TOTAL_CALORIES("totalCalories", TotalCaloriesBurnedRecord::class, "kilocalorie"),
54
+ MINDFULNESS("mindfulness", MindfulnessSessionRecord::class, "minute");
32
55
 
33
56
  val readPermission: String
34
57
  get() = HealthPermission.getReadPermission(recordClass)
@@ -1,20 +1,32 @@
1
1
  package app.capgo.plugin.health
2
2
 
3
3
  import androidx.health.connect.client.HealthConnectClient
4
+ import androidx.health.connect.client.feature.ExperimentalMindfulnessSessionApi
4
5
  import androidx.health.connect.client.permission.HealthPermission
5
6
  import androidx.health.connect.client.request.AggregateRequest
6
7
  import androidx.health.connect.client.records.ActiveCaloriesBurnedRecord
8
+ import androidx.health.connect.client.records.BasalBodyTemperatureRecord
9
+ import androidx.health.connect.client.records.BasalMetabolicRateRecord
10
+ import androidx.health.connect.client.records.BloodGlucoseRecord
11
+ import androidx.health.connect.client.records.BloodPressureRecord
12
+ import androidx.health.connect.client.records.BodyFatRecord
13
+ import androidx.health.connect.client.records.BodyTemperatureRecord
7
14
  import androidx.health.connect.client.records.DistanceRecord
8
15
  import androidx.health.connect.client.records.ExerciseSessionRecord
16
+ import androidx.health.connect.client.records.FloorsClimbedRecord
9
17
  import androidx.health.connect.client.records.HeartRateRecord
10
18
  import androidx.health.connect.client.records.HeartRateVariabilityRmssdRecord
19
+ import androidx.health.connect.client.records.HeightRecord
20
+ import androidx.health.connect.client.records.MindfulnessSessionRecord
11
21
  import androidx.health.connect.client.records.OxygenSaturationRecord
12
22
  import androidx.health.connect.client.records.Record
13
23
  import androidx.health.connect.client.records.RespiratoryRateRecord
14
24
  import androidx.health.connect.client.records.RestingHeartRateRecord
15
25
  import androidx.health.connect.client.records.SleepSessionRecord
16
26
  import androidx.health.connect.client.records.StepsRecord
27
+ import androidx.health.connect.client.records.TotalCaloriesBurnedRecord
17
28
  import androidx.health.connect.client.records.WeightRecord
29
+ import androidx.health.connect.client.records.metadata.Metadata
18
30
  import androidx.health.connect.client.request.ReadRecordsRequest
19
31
  import androidx.health.connect.client.time.TimeRangeFilter
20
32
  import androidx.health.connect.client.units.Energy
@@ -22,18 +34,18 @@ import androidx.health.connect.client.units.Length
22
34
  import androidx.health.connect.client.units.Mass
23
35
  import androidx.health.connect.client.units.Percentage
24
36
  import androidx.health.connect.client.units.Power
25
- import androidx.health.connect.client.records.metadata.Metadata
26
- import java.time.Duration
27
37
  import com.getcapacitor.JSArray
28
38
  import com.getcapacitor.JSObject
39
+ import java.time.Duration
29
40
  import java.time.Instant
30
41
  import java.time.ZoneId
31
42
  import java.time.ZoneOffset
32
43
  import java.time.format.DateTimeFormatter
33
- import kotlin.math.min
34
44
  import kotlin.collections.buildSet
45
+ import kotlin.math.min
35
46
  import kotlinx.coroutines.CancellationException
36
47
 
48
+ @OptIn(ExperimentalMindfulnessSessionApi::class)
37
49
  class HealthManager {
38
50
 
39
51
  private val formatter: DateTimeFormatter = DateTimeFormatter.ISO_INSTANT
@@ -211,6 +223,119 @@ class HealthManager {
211
223
  )
212
224
  samples.add(record.time to payload)
213
225
  }
226
+ HealthDataType.BLOOD_PRESSURE -> readRecords(client, BloodPressureRecord::class, startTime, endTime, limit) { record ->
227
+ val payload = createSamplePayload(
228
+ dataType,
229
+ record.time,
230
+ record.time,
231
+ record.systolic.inMillimetersOfMercury,
232
+ record.metadata
233
+ )
234
+ payload.put("systolic", record.systolic.inMillimetersOfMercury)
235
+ payload.put("diastolic", record.diastolic.inMillimetersOfMercury)
236
+ samples.add(record.time to payload)
237
+ }
238
+ HealthDataType.BLOOD_GLUCOSE -> readRecords(client, BloodGlucoseRecord::class, startTime, endTime, limit) { record ->
239
+ val payload = createSamplePayload(
240
+ dataType,
241
+ record.time,
242
+ record.time,
243
+ record.level.inMilligramsPerDeciliter,
244
+ record.metadata
245
+ )
246
+ samples.add(record.time to payload)
247
+ }
248
+ HealthDataType.BODY_TEMPERATURE -> readRecords(client, BodyTemperatureRecord::class, startTime, endTime, limit) { record ->
249
+ val payload = createSamplePayload(
250
+ dataType,
251
+ record.time,
252
+ record.time,
253
+ record.temperature.inCelsius,
254
+ record.metadata
255
+ )
256
+ samples.add(record.time to payload)
257
+ }
258
+ HealthDataType.HEIGHT -> readRecords(client, HeightRecord::class, startTime, endTime, limit) { record ->
259
+ val payload = createSamplePayload(
260
+ dataType,
261
+ record.time,
262
+ record.time,
263
+ record.height.inMeters * 100.0, // Convert to centimeters
264
+ record.metadata
265
+ )
266
+ samples.add(record.time to payload)
267
+ }
268
+ HealthDataType.FLIGHTS_CLIMBED -> readRecords(client, FloorsClimbedRecord::class, startTime, endTime, limit) { record ->
269
+ val payload = createSamplePayload(
270
+ dataType,
271
+ record.startTime,
272
+ record.endTime,
273
+ record.floors,
274
+ record.metadata
275
+ )
276
+ samples.add(record.startTime to payload)
277
+ }
278
+ HealthDataType.DISTANCE_CYCLING -> readRecords(client, DistanceRecord::class, startTime, endTime, limit) { record ->
279
+ val payload = createSamplePayload(
280
+ dataType,
281
+ record.startTime,
282
+ record.endTime,
283
+ record.distance.inMeters,
284
+ record.metadata
285
+ )
286
+ samples.add(record.startTime to payload)
287
+ }
288
+ HealthDataType.BODY_FAT -> readRecords(client, BodyFatRecord::class, startTime, endTime, limit) { record ->
289
+ val payload = createSamplePayload(
290
+ dataType,
291
+ record.time,
292
+ record.time,
293
+ record.percentage.value,
294
+ record.metadata
295
+ )
296
+ samples.add(record.time to payload)
297
+ }
298
+ HealthDataType.BASAL_BODY_TEMPERATURE -> readRecords(client, BasalBodyTemperatureRecord::class, startTime, endTime, limit) { record ->
299
+ val payload = createSamplePayload(
300
+ dataType,
301
+ record.time,
302
+ record.time,
303
+ record.temperature.inCelsius,
304
+ record.metadata
305
+ )
306
+ samples.add(record.time to payload)
307
+ }
308
+ HealthDataType.BASAL_CALORIES -> readRecords(client, BasalMetabolicRateRecord::class, startTime, endTime, limit) { record ->
309
+ val payload = createSamplePayload(
310
+ dataType,
311
+ record.time,
312
+ record.time,
313
+ record.basalMetabolicRate.inKilocaloriesPerDay,
314
+ record.metadata
315
+ )
316
+ samples.add(record.time to payload)
317
+ }
318
+ HealthDataType.TOTAL_CALORIES -> readRecords(client, TotalCaloriesBurnedRecord::class, startTime, endTime, limit) { record ->
319
+ val payload = createSamplePayload(
320
+ dataType,
321
+ record.startTime,
322
+ record.endTime,
323
+ record.energy.inKilocalories,
324
+ record.metadata
325
+ )
326
+ samples.add(record.startTime to payload)
327
+ }
328
+ HealthDataType.MINDFULNESS -> readRecords(client, MindfulnessSessionRecord::class, startTime, endTime, limit) { record ->
329
+ val durationMinutes = Duration.between(record.startTime, record.endTime).toMinutes().toDouble()
330
+ val payload = createSamplePayload(
331
+ dataType,
332
+ record.startTime,
333
+ record.endTime,
334
+ durationMinutes,
335
+ record.metadata
336
+ )
337
+ samples.add(record.startTime to payload)
338
+ }
214
339
  }
215
340
 
216
341
  val sorted = samples.sortedBy { it.first }
@@ -257,8 +382,12 @@ class HealthManager {
257
382
  value: Double,
258
383
  startTime: Instant,
259
384
  endTime: Instant,
260
- metadata: Map<String, String>?
385
+ metadata: Map<String, String>?,
386
+ systolic: Double?,
387
+ diastolic: Double?
261
388
  ) {
389
+ val recordMetadata = Metadata.manualEntry()
390
+
262
391
  when (dataType) {
263
392
  HealthDataType.STEPS -> {
264
393
  val record = StepsRecord(
@@ -266,7 +395,8 @@ class HealthManager {
266
395
  startZoneOffset = zoneOffset(startTime),
267
396
  endTime = endTime,
268
397
  endZoneOffset = zoneOffset(endTime),
269
- count = value.toLong().coerceAtLeast(0)
398
+ count = value.toLong().coerceAtLeast(0),
399
+ metadata = recordMetadata
270
400
  )
271
401
  client.insertRecords(listOf(record))
272
402
  }
@@ -276,7 +406,8 @@ class HealthManager {
276
406
  startZoneOffset = zoneOffset(startTime),
277
407
  endTime = endTime,
278
408
  endZoneOffset = zoneOffset(endTime),
279
- distance = Length.meters(value)
409
+ distance = Length.meters(value),
410
+ metadata = recordMetadata
280
411
  )
281
412
  client.insertRecords(listOf(record))
282
413
  }
@@ -286,7 +417,8 @@ class HealthManager {
286
417
  startZoneOffset = zoneOffset(startTime),
287
418
  endTime = endTime,
288
419
  endZoneOffset = zoneOffset(endTime),
289
- energy = Energy.kilocalories(value)
420
+ energy = Energy.kilocalories(value),
421
+ metadata = recordMetadata
290
422
  )
291
423
  client.insertRecords(listOf(record))
292
424
  }
@@ -294,7 +426,8 @@ class HealthManager {
294
426
  val record = WeightRecord(
295
427
  time = startTime,
296
428
  zoneOffset = zoneOffset(startTime),
297
- weight = Mass.kilograms(value)
429
+ weight = Mass.kilograms(value),
430
+ metadata = recordMetadata
298
431
  )
299
432
  client.insertRecords(listOf(record))
300
433
  }
@@ -305,7 +438,8 @@ class HealthManager {
305
438
  startZoneOffset = zoneOffset(startTime),
306
439
  endTime = endTime,
307
440
  endZoneOffset = zoneOffset(endTime),
308
- samples = samples
441
+ samples = samples,
442
+ metadata = recordMetadata
309
443
  )
310
444
  client.insertRecords(listOf(record))
311
445
  }
@@ -314,7 +448,8 @@ class HealthManager {
314
448
  startTime = startTime,
315
449
  startZoneOffset = zoneOffset(startTime),
316
450
  endTime = endTime,
317
- endZoneOffset = zoneOffset(endTime)
451
+ endZoneOffset = zoneOffset(endTime),
452
+ metadata = recordMetadata
318
453
  )
319
454
  client.insertRecords(listOf(record))
320
455
  }
@@ -322,7 +457,8 @@ class HealthManager {
322
457
  val record = RespiratoryRateRecord(
323
458
  time = startTime,
324
459
  zoneOffset = zoneOffset(startTime),
325
- rate = value
460
+ rate = value,
461
+ metadata = recordMetadata
326
462
  )
327
463
  client.insertRecords(listOf(record))
328
464
  }
@@ -330,7 +466,8 @@ class HealthManager {
330
466
  val record = OxygenSaturationRecord(
331
467
  time = startTime,
332
468
  zoneOffset = zoneOffset(startTime),
333
- percentage = Percentage(value)
469
+ percentage = Percentage(value),
470
+ metadata = recordMetadata
334
471
  )
335
472
  client.insertRecords(listOf(record))
336
473
  }
@@ -338,7 +475,8 @@ class HealthManager {
338
475
  val record = RestingHeartRateRecord(
339
476
  time = startTime,
340
477
  zoneOffset = zoneOffset(startTime),
341
- beatsPerMinute = value.toBpmLong()
478
+ beatsPerMinute = value.toBpmLong(),
479
+ metadata = recordMetadata
342
480
  )
343
481
  client.insertRecords(listOf(record))
344
482
  }
@@ -346,7 +484,119 @@ class HealthManager {
346
484
  val record = HeartRateVariabilityRmssdRecord(
347
485
  time = startTime,
348
486
  zoneOffset = zoneOffset(startTime),
349
- heartRateVariabilityMillis = value
487
+ heartRateVariabilityMillis = value,
488
+ metadata = recordMetadata
489
+ )
490
+ client.insertRecords(listOf(record))
491
+ }
492
+ HealthDataType.BLOOD_PRESSURE -> {
493
+ if (systolic == null || diastolic == null) {
494
+ throw IllegalArgumentException("Blood pressure requires both systolic and diastolic values")
495
+ }
496
+ val record = BloodPressureRecord(
497
+ time = startTime,
498
+ zoneOffset = zoneOffset(startTime),
499
+ metadata = recordMetadata,
500
+ systolic = androidx.health.connect.client.units.Pressure.millimetersOfMercury(systolic),
501
+ diastolic = androidx.health.connect.client.units.Pressure.millimetersOfMercury(diastolic)
502
+ )
503
+ client.insertRecords(listOf(record))
504
+ }
505
+ HealthDataType.BLOOD_GLUCOSE -> {
506
+ val record = BloodGlucoseRecord(
507
+ time = startTime,
508
+ zoneOffset = zoneOffset(startTime),
509
+ metadata = recordMetadata,
510
+ level = androidx.health.connect.client.units.BloodGlucose.milligramsPerDeciliter(value)
511
+ )
512
+ client.insertRecords(listOf(record))
513
+ }
514
+ HealthDataType.BODY_TEMPERATURE -> {
515
+ val record = BodyTemperatureRecord(
516
+ time = startTime,
517
+ zoneOffset = zoneOffset(startTime),
518
+ metadata = recordMetadata,
519
+ temperature = androidx.health.connect.client.units.Temperature.celsius(value)
520
+ )
521
+ client.insertRecords(listOf(record))
522
+ }
523
+ HealthDataType.HEIGHT -> {
524
+ val record = HeightRecord(
525
+ time = startTime,
526
+ zoneOffset = zoneOffset(startTime),
527
+ height = Length.meters(value / 100.0), // Convert from centimeters to meters
528
+ metadata = recordMetadata
529
+ )
530
+ client.insertRecords(listOf(record))
531
+ }
532
+ HealthDataType.FLIGHTS_CLIMBED -> {
533
+ val record = FloorsClimbedRecord(
534
+ startTime = startTime,
535
+ startZoneOffset = zoneOffset(startTime),
536
+ endTime = endTime,
537
+ endZoneOffset = zoneOffset(endTime),
538
+ floors = value,
539
+ metadata = recordMetadata
540
+ )
541
+ client.insertRecords(listOf(record))
542
+ }
543
+ HealthDataType.DISTANCE_CYCLING -> {
544
+ val record = DistanceRecord(
545
+ startTime = startTime,
546
+ startZoneOffset = zoneOffset(startTime),
547
+ endTime = endTime,
548
+ endZoneOffset = zoneOffset(endTime),
549
+ distance = Length.meters(value),
550
+ metadata = recordMetadata
551
+ )
552
+ client.insertRecords(listOf(record))
553
+ }
554
+ HealthDataType.BODY_FAT -> {
555
+ val record = BodyFatRecord(
556
+ time = startTime,
557
+ zoneOffset = zoneOffset(startTime),
558
+ percentage = Percentage(value),
559
+ metadata = recordMetadata
560
+ )
561
+ client.insertRecords(listOf(record))
562
+ }
563
+ HealthDataType.BASAL_BODY_TEMPERATURE -> {
564
+ val record = BasalBodyTemperatureRecord(
565
+ time = startTime,
566
+ zoneOffset = zoneOffset(startTime),
567
+ metadata = recordMetadata,
568
+ temperature = androidx.health.connect.client.units.Temperature.celsius(value)
569
+ )
570
+ client.insertRecords(listOf(record))
571
+ }
572
+ HealthDataType.BASAL_CALORIES -> {
573
+ val record = BasalMetabolicRateRecord(
574
+ time = startTime,
575
+ zoneOffset = zoneOffset(startTime),
576
+ basalMetabolicRate = Power.kilocaloriesPerDay(value),
577
+ metadata = recordMetadata
578
+ )
579
+ client.insertRecords(listOf(record))
580
+ }
581
+ HealthDataType.TOTAL_CALORIES -> {
582
+ val record = TotalCaloriesBurnedRecord(
583
+ startTime = startTime,
584
+ startZoneOffset = zoneOffset(startTime),
585
+ endTime = endTime,
586
+ endZoneOffset = zoneOffset(endTime),
587
+ energy = Energy.kilocalories(value),
588
+ metadata = recordMetadata
589
+ )
590
+ client.insertRecords(listOf(record))
591
+ }
592
+ HealthDataType.MINDFULNESS -> {
593
+ val record = MindfulnessSessionRecord(
594
+ startTime = startTime,
595
+ startZoneOffset = zoneOffset(startTime),
596
+ endTime = endTime,
597
+ endZoneOffset = zoneOffset(endTime),
598
+ metadata = recordMetadata,
599
+ mindfulnessSessionType = MindfulnessSessionRecord.MINDFULNESS_SESSION_TYPE_UNKNOWN
350
600
  )
351
601
  client.insertRecords(listOf(record))
352
602
  }
@@ -245,11 +245,14 @@ class HealthPlugin : Plugin() {
245
245
  }
246
246
  map.takeIf { it.isNotEmpty() }
247
247
  }
248
+
249
+ val systolic = call.getDouble("systolic")
250
+ val diastolic = call.getDouble("diastolic")
248
251
 
249
252
  pluginScope.launch {
250
253
  val client = getClientOrReject(call) ?: return@launch
251
254
  try {
252
- manager.saveSample(client, dataType, value, startInstant, endInstant, metadata)
255
+ manager.saveSample(client, dataType, value, startInstant, endInstant, metadata, systolic, diastolic)
253
256
  call.resolve()
254
257
  } catch (e: Exception) {
255
258
  call.reject(e.message ?: "Failed to save sample.", null, e)
package/dist/docs.json CHANGED
@@ -397,6 +397,20 @@
397
397
  "SleepState"
398
398
  ],
399
399
  "type": "SleepState"
400
+ },
401
+ {
402
+ "name": "systolic",
403
+ "tags": [],
404
+ "docs": "For blood pressure data, the systolic value in mmHg.",
405
+ "complexTypes": [],
406
+ "type": "number | undefined"
407
+ },
408
+ {
409
+ "name": "diastolic",
410
+ "tags": [],
411
+ "docs": "For blood pressure data, the diastolic value in mmHg.",
412
+ "complexTypes": [],
413
+ "type": "number | undefined"
400
414
  }
401
415
  ]
402
416
  },
@@ -500,6 +514,20 @@
500
514
  "Record"
501
515
  ],
502
516
  "type": "Record<string, string>"
517
+ },
518
+ {
519
+ "name": "systolic",
520
+ "tags": [],
521
+ "docs": "For blood pressure data, the systolic value in mmHg. Required when dataType is 'bloodPressure'.",
522
+ "complexTypes": [],
523
+ "type": "number | undefined"
524
+ },
525
+ {
526
+ "name": "diastolic",
527
+ "tags": [],
528
+ "docs": "For blood pressure data, the diastolic value in mmHg. Required when dataType is 'bloodPressure'.",
529
+ "complexTypes": [],
530
+ "type": "number | undefined"
503
531
  }
504
532
  ]
505
533
  },
@@ -811,6 +839,54 @@
811
839
  {
812
840
  "text": "'heartRateVariability'",
813
841
  "complexTypes": []
842
+ },
843
+ {
844
+ "text": "'bloodPressure'",
845
+ "complexTypes": []
846
+ },
847
+ {
848
+ "text": "'bloodGlucose'",
849
+ "complexTypes": []
850
+ },
851
+ {
852
+ "text": "'bodyTemperature'",
853
+ "complexTypes": []
854
+ },
855
+ {
856
+ "text": "'height'",
857
+ "complexTypes": []
858
+ },
859
+ {
860
+ "text": "'flightsClimbed'",
861
+ "complexTypes": []
862
+ },
863
+ {
864
+ "text": "'exerciseTime'",
865
+ "complexTypes": []
866
+ },
867
+ {
868
+ "text": "'distanceCycling'",
869
+ "complexTypes": []
870
+ },
871
+ {
872
+ "text": "'bodyFat'",
873
+ "complexTypes": []
874
+ },
875
+ {
876
+ "text": "'basalBodyTemperature'",
877
+ "complexTypes": []
878
+ },
879
+ {
880
+ "text": "'basalCalories'",
881
+ "complexTypes": []
882
+ },
883
+ {
884
+ "text": "'totalCalories'",
885
+ "complexTypes": []
886
+ },
887
+ {
888
+ "text": "'mindfulness'",
889
+ "complexTypes": []
814
890
  }
815
891
  ]
816
892
  },
@@ -850,6 +926,26 @@
850
926
  {
851
927
  "text": "'millisecond'",
852
928
  "complexTypes": []
929
+ },
930
+ {
931
+ "text": "'mmHg'",
932
+ "complexTypes": []
933
+ },
934
+ {
935
+ "text": "'mg/dL'",
936
+ "complexTypes": []
937
+ },
938
+ {
939
+ "text": "'celsius'",
940
+ "complexTypes": []
941
+ },
942
+ {
943
+ "text": "'fahrenheit'",
944
+ "complexTypes": []
945
+ },
946
+ {
947
+ "text": "'centimeter'",
948
+ "complexTypes": []
853
949
  }
854
950
  ]
855
951
  },
@@ -1,5 +1,5 @@
1
- export type HealthDataType = 'steps' | 'distance' | 'calories' | 'heartRate' | 'weight' | 'sleep' | 'respiratoryRate' | 'oxygenSaturation' | 'restingHeartRate' | 'heartRateVariability';
2
- export type HealthUnit = 'count' | 'meter' | 'kilocalorie' | 'bpm' | 'kilogram' | 'minute' | 'percent' | 'millisecond';
1
+ export type HealthDataType = 'steps' | 'distance' | 'calories' | 'heartRate' | 'weight' | 'sleep' | 'respiratoryRate' | 'oxygenSaturation' | 'restingHeartRate' | 'heartRateVariability' | 'bloodPressure' | 'bloodGlucose' | 'bodyTemperature' | 'height' | 'flightsClimbed' | 'exerciseTime' | 'distanceCycling' | 'bodyFat' | 'basalBodyTemperature' | 'basalCalories' | 'totalCalories' | 'mindfulness';
2
+ export type HealthUnit = 'count' | 'meter' | 'kilocalorie' | 'bpm' | 'kilogram' | 'minute' | 'percent' | 'millisecond' | 'mmHg' | 'mg/dL' | 'celsius' | 'fahrenheit' | 'centimeter';
3
3
  export interface AuthorizationOptions {
4
4
  /** Data types that should be readable after authorization. */
5
5
  read?: HealthDataType[];
@@ -41,6 +41,10 @@ export interface HealthSample {
41
41
  sourceId?: string;
42
42
  /** For sleep data, indicates the sleep state (e.g., 'asleep', 'awake', 'rem', 'deep', 'light'). */
43
43
  sleepState?: SleepState;
44
+ /** For blood pressure data, the systolic value in mmHg. */
45
+ systolic?: number;
46
+ /** For blood pressure data, the diastolic value in mmHg. */
47
+ diastolic?: number;
44
48
  }
45
49
  export interface ReadSamplesResult {
46
50
  samples: HealthSample[];
@@ -106,6 +110,10 @@ export interface WriteSampleOptions {
106
110
  endDate?: string;
107
111
  /** Metadata key-value pairs forwarded to the native APIs where supported. */
108
112
  metadata?: Record<string, string>;
113
+ /** For blood pressure data, the systolic value in mmHg. Required when dataType is 'bloodPressure'. */
114
+ systolic?: number;
115
+ /** For blood pressure data, the diastolic value in mmHg. Required when dataType is 'bloodPressure'. */
116
+ diastolic?: number;
109
117
  }
110
118
  export type BucketType = 'hour' | 'day' | 'week' | 'month';
111
119
  export type AggregationType = 'sum' | 'average' | 'min' | 'max';
@@ -1 +1 @@
1
- {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["export type HealthDataType =\n | 'steps'\n | 'distance'\n | 'calories'\n | 'heartRate'\n | 'weight'\n | 'sleep'\n | 'respiratoryRate'\n | 'oxygenSaturation'\n | 'restingHeartRate'\n | 'heartRateVariability';\n\nexport type HealthUnit = 'count' | 'meter' | 'kilocalorie' | 'bpm' | 'kilogram' | 'minute' | 'percent' | 'millisecond';\n\nexport interface AuthorizationOptions {\n /** Data types that should be readable after authorization. */\n read?: HealthDataType[];\n /** Data types that should be writable after authorization. */\n write?: HealthDataType[];\n}\n\nexport interface AuthorizationStatus {\n readAuthorized: HealthDataType[];\n readDenied: HealthDataType[];\n writeAuthorized: HealthDataType[];\n writeDenied: HealthDataType[];\n}\n\nexport interface AvailabilityResult {\n available: boolean;\n /** Platform specific details (for debugging/diagnostics). */\n platform?: 'ios' | 'android' | 'web';\n reason?: string;\n}\n\nexport interface QueryOptions {\n /** The type of data to retrieve from the health store. */\n dataType: HealthDataType;\n /** Inclusive ISO 8601 start date (defaults to now - 1 day). */\n startDate?: string;\n /** Exclusive ISO 8601 end date (defaults to now). */\n endDate?: string;\n /** Maximum number of samples to return (defaults to 100). */\n limit?: number;\n /** Return results sorted ascending by start date (defaults to false). */\n ascending?: boolean;\n}\n\nexport type SleepState = 'inBed' | 'asleep' | 'awake' | 'rem' | 'deep' | 'light';\n\nexport interface HealthSample {\n dataType: HealthDataType;\n value: number;\n unit: HealthUnit;\n startDate: string;\n endDate: string;\n sourceName?: string;\n sourceId?: string;\n /** For sleep data, indicates the sleep state (e.g., 'asleep', 'awake', 'rem', 'deep', 'light'). */\n sleepState?: SleepState;\n}\n\nexport interface ReadSamplesResult {\n samples: HealthSample[];\n}\n\nexport type WorkoutType =\n | 'running'\n | 'cycling'\n | 'walking'\n | 'swimming'\n | 'yoga'\n | 'strengthTraining'\n | 'hiking'\n | 'tennis'\n | 'basketball'\n | 'soccer'\n | 'americanFootball'\n | 'baseball'\n | 'crossTraining'\n | 'elliptical'\n | 'rowing'\n | 'stairClimbing'\n | 'traditionalStrengthTraining'\n | 'waterFitness'\n | 'waterPolo'\n | 'waterSports'\n | 'wrestling'\n | 'other';\n\nexport interface QueryWorkoutsOptions {\n /** Optional workout type filter. If omitted, all workout types are returned. */\n workoutType?: WorkoutType;\n /** Inclusive ISO 8601 start date (defaults to now - 1 day). */\n startDate?: string;\n /** Exclusive ISO 8601 end date (defaults to now). */\n endDate?: string;\n /** Maximum number of workouts to return (defaults to 100). */\n limit?: number;\n /** Return results sorted ascending by start date (defaults to false). */\n ascending?: boolean;\n /**\n * Anchor for pagination. Use the anchor returned from a previous query to continue from that point.\n * On iOS, this uses HKQueryAnchor. On Android, this uses Health Connect's pageToken.\n * Omit this parameter to start from the beginning.\n */\n anchor?: string;\n}\n\nexport interface Workout {\n /** The type of workout. */\n workoutType: WorkoutType;\n /** Duration of the workout in seconds. */\n duration: number;\n /** Total energy burned in kilocalories (if available). */\n totalEnergyBurned?: number;\n /** Total distance in meters (if available). */\n totalDistance?: number;\n /** ISO 8601 start date of the workout. */\n startDate: string;\n /** ISO 8601 end date of the workout. */\n endDate: string;\n /** Source name that recorded the workout. */\n sourceName?: string;\n /** Source bundle identifier. */\n sourceId?: string;\n /** Additional metadata (if available). */\n metadata?: Record<string, string>;\n}\n\nexport interface QueryWorkoutsResult {\n workouts: Workout[];\n /**\n * Anchor for the next page of results. Pass this value as the anchor parameter in the next query\n * to continue pagination. If undefined or null, there are no more results.\n */\n anchor?: string;\n}\n\nexport interface WriteSampleOptions {\n dataType: HealthDataType;\n value: number;\n /**\n * Optional unit override. If omitted, the default unit for the data type is used\n * (count for `steps`, meter for `distance`, kilocalorie for `calories`, bpm for `heartRate`, kilogram for `weight`).\n */\n unit?: HealthUnit;\n /** ISO 8601 start date for the sample. Defaults to now. */\n startDate?: string;\n /** ISO 8601 end date for the sample. Defaults to startDate. */\n endDate?: string;\n /** Metadata key-value pairs forwarded to the native APIs where supported. */\n metadata?: Record<string, string>;\n}\n\nexport type BucketType = 'hour' | 'day' | 'week' | 'month';\n\nexport type AggregationType = 'sum' | 'average' | 'min' | 'max';\n\nexport interface QueryAggregatedOptions {\n /** The type of data to aggregate from the health store. */\n dataType: HealthDataType;\n /** Inclusive ISO 8601 start date (defaults to now - 1 day). */\n startDate?: string;\n /** Exclusive ISO 8601 end date (defaults to now). */\n endDate?: string;\n /** Time bucket for aggregation (defaults to 'day'). */\n bucket?: BucketType;\n /** Aggregation operation to perform (defaults to 'sum'). */\n aggregation?: AggregationType;\n}\n\nexport interface AggregatedSample {\n /** ISO 8601 start date of the bucket. */\n startDate: string;\n /** ISO 8601 end date of the bucket. */\n endDate: string;\n /** Aggregated value for the bucket. */\n value: number;\n /** Unit of the aggregated value. */\n unit: HealthUnit;\n}\n\nexport interface QueryAggregatedResult {\n samples: AggregatedSample[];\n}\n\nexport interface HealthPlugin {\n /** Returns whether the current platform supports the native health SDK. */\n isAvailable(): Promise<AvailabilityResult>;\n /** Requests read/write access to the provided data types. */\n requestAuthorization(options: AuthorizationOptions): Promise<AuthorizationStatus>;\n /** Checks authorization status for the provided data types without prompting the user. */\n checkAuthorization(options: AuthorizationOptions): Promise<AuthorizationStatus>;\n /** Reads samples for the given data type within the specified time frame. */\n readSamples(options: QueryOptions): Promise<ReadSamplesResult>;\n /** Writes a single sample to the native health store. */\n saveSample(options: WriteSampleOptions): Promise<void>;\n\n /**\n * Get the native Capacitor plugin version\n *\n * @returns {Promise<{ version: string }>} a Promise with version for this device\n * @throws An error if something went wrong\n */\n getPluginVersion(): Promise<{ version: string }>;\n\n /**\n * Opens the Health Connect settings screen (Android only).\n * On iOS, this method does nothing.\n *\n * Use this to direct users to manage their Health Connect permissions\n * or to install Health Connect if not available.\n *\n * @throws An error if Health Connect settings cannot be opened\n */\n openHealthConnectSettings(): Promise<void>;\n\n /**\n * Shows the app's privacy policy for Health Connect (Android only).\n * On iOS, this method does nothing.\n *\n * This displays the same privacy policy screen that Health Connect shows\n * when the user taps \"Privacy policy\" in the permissions dialog.\n *\n * The privacy policy URL can be configured by adding a string resource\n * named \"health_connect_privacy_policy_url\" in your app's strings.xml,\n * or by placing an HTML file at www/privacypolicy.html in your assets.\n *\n * @throws An error if the privacy policy cannot be displayed\n */\n showPrivacyPolicy(): Promise<void>;\n\n /**\n * Queries workout sessions from the native health store.\n * Supported on iOS (HealthKit) and Android (Health Connect).\n *\n * @param options Query options including optional workout type filter, date range, limit, and sort order\n * @returns A promise that resolves with the workout sessions\n * @throws An error if something went wrong\n */\n queryWorkouts(options: QueryWorkoutsOptions): Promise<QueryWorkoutsResult>;\n\n /**\n * Queries aggregated health data from the native health store.\n * Aggregates data into time buckets (hour, day, week, month) with operations like sum, average, min, or max.\n * This is more efficient than fetching individual samples for large date ranges.\n *\n * Supported on iOS (HealthKit) and Android (Health Connect).\n *\n * @param options Query options including data type, date range, bucket size, and aggregation type\n * @returns A promise that resolves with the aggregated samples\n * @throws An error if something went wrong\n */\n queryAggregated(options: QueryAggregatedOptions): Promise<QueryAggregatedResult>;\n}\n"]}
1
+ {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["export type HealthDataType =\n | 'steps'\n | 'distance'\n | 'calories'\n | 'heartRate'\n | 'weight'\n | 'sleep'\n | 'respiratoryRate'\n | 'oxygenSaturation'\n | 'restingHeartRate'\n | 'heartRateVariability'\n | 'bloodPressure'\n | 'bloodGlucose'\n | 'bodyTemperature'\n | 'height'\n | 'flightsClimbed'\n | 'exerciseTime'\n | 'distanceCycling'\n | 'bodyFat'\n | 'basalBodyTemperature'\n | 'basalCalories'\n | 'totalCalories'\n | 'mindfulness';\n\nexport type HealthUnit =\n | 'count'\n | 'meter'\n | 'kilocalorie'\n | 'bpm'\n | 'kilogram'\n | 'minute'\n | 'percent'\n | 'millisecond'\n | 'mmHg'\n | 'mg/dL'\n | 'celsius'\n | 'fahrenheit'\n | 'centimeter';\n\nexport interface AuthorizationOptions {\n /** Data types that should be readable after authorization. */\n read?: HealthDataType[];\n /** Data types that should be writable after authorization. */\n write?: HealthDataType[];\n}\n\nexport interface AuthorizationStatus {\n readAuthorized: HealthDataType[];\n readDenied: HealthDataType[];\n writeAuthorized: HealthDataType[];\n writeDenied: HealthDataType[];\n}\n\nexport interface AvailabilityResult {\n available: boolean;\n /** Platform specific details (for debugging/diagnostics). */\n platform?: 'ios' | 'android' | 'web';\n reason?: string;\n}\n\nexport interface QueryOptions {\n /** The type of data to retrieve from the health store. */\n dataType: HealthDataType;\n /** Inclusive ISO 8601 start date (defaults to now - 1 day). */\n startDate?: string;\n /** Exclusive ISO 8601 end date (defaults to now). */\n endDate?: string;\n /** Maximum number of samples to return (defaults to 100). */\n limit?: number;\n /** Return results sorted ascending by start date (defaults to false). */\n ascending?: boolean;\n}\n\nexport type SleepState = 'inBed' | 'asleep' | 'awake' | 'rem' | 'deep' | 'light';\n\nexport interface HealthSample {\n dataType: HealthDataType;\n value: number;\n unit: HealthUnit;\n startDate: string;\n endDate: string;\n sourceName?: string;\n sourceId?: string;\n /** For sleep data, indicates the sleep state (e.g., 'asleep', 'awake', 'rem', 'deep', 'light'). */\n sleepState?: SleepState;\n /** For blood pressure data, the systolic value in mmHg. */\n systolic?: number;\n /** For blood pressure data, the diastolic value in mmHg. */\n diastolic?: number;\n}\n\nexport interface ReadSamplesResult {\n samples: HealthSample[];\n}\n\nexport type WorkoutType =\n | 'running'\n | 'cycling'\n | 'walking'\n | 'swimming'\n | 'yoga'\n | 'strengthTraining'\n | 'hiking'\n | 'tennis'\n | 'basketball'\n | 'soccer'\n | 'americanFootball'\n | 'baseball'\n | 'crossTraining'\n | 'elliptical'\n | 'rowing'\n | 'stairClimbing'\n | 'traditionalStrengthTraining'\n | 'waterFitness'\n | 'waterPolo'\n | 'waterSports'\n | 'wrestling'\n | 'other';\n\nexport interface QueryWorkoutsOptions {\n /** Optional workout type filter. If omitted, all workout types are returned. */\n workoutType?: WorkoutType;\n /** Inclusive ISO 8601 start date (defaults to now - 1 day). */\n startDate?: string;\n /** Exclusive ISO 8601 end date (defaults to now). */\n endDate?: string;\n /** Maximum number of workouts to return (defaults to 100). */\n limit?: number;\n /** Return results sorted ascending by start date (defaults to false). */\n ascending?: boolean;\n /**\n * Anchor for pagination. Use the anchor returned from a previous query to continue from that point.\n * On iOS, this uses HKQueryAnchor. On Android, this uses Health Connect's pageToken.\n * Omit this parameter to start from the beginning.\n */\n anchor?: string;\n}\n\nexport interface Workout {\n /** The type of workout. */\n workoutType: WorkoutType;\n /** Duration of the workout in seconds. */\n duration: number;\n /** Total energy burned in kilocalories (if available). */\n totalEnergyBurned?: number;\n /** Total distance in meters (if available). */\n totalDistance?: number;\n /** ISO 8601 start date of the workout. */\n startDate: string;\n /** ISO 8601 end date of the workout. */\n endDate: string;\n /** Source name that recorded the workout. */\n sourceName?: string;\n /** Source bundle identifier. */\n sourceId?: string;\n /** Additional metadata (if available). */\n metadata?: Record<string, string>;\n}\n\nexport interface QueryWorkoutsResult {\n workouts: Workout[];\n /**\n * Anchor for the next page of results. Pass this value as the anchor parameter in the next query\n * to continue pagination. If undefined or null, there are no more results.\n */\n anchor?: string;\n}\n\nexport interface WriteSampleOptions {\n dataType: HealthDataType;\n value: number;\n /**\n * Optional unit override. If omitted, the default unit for the data type is used\n * (count for `steps`, meter for `distance`, kilocalorie for `calories`, bpm for `heartRate`, kilogram for `weight`).\n */\n unit?: HealthUnit;\n /** ISO 8601 start date for the sample. Defaults to now. */\n startDate?: string;\n /** ISO 8601 end date for the sample. Defaults to startDate. */\n endDate?: string;\n /** Metadata key-value pairs forwarded to the native APIs where supported. */\n metadata?: Record<string, string>;\n /** For blood pressure data, the systolic value in mmHg. Required when dataType is 'bloodPressure'. */\n systolic?: number;\n /** For blood pressure data, the diastolic value in mmHg. Required when dataType is 'bloodPressure'. */\n diastolic?: number;\n}\n\nexport type BucketType = 'hour' | 'day' | 'week' | 'month';\n\nexport type AggregationType = 'sum' | 'average' | 'min' | 'max';\n\nexport interface QueryAggregatedOptions {\n /** The type of data to aggregate from the health store. */\n dataType: HealthDataType;\n /** Inclusive ISO 8601 start date (defaults to now - 1 day). */\n startDate?: string;\n /** Exclusive ISO 8601 end date (defaults to now). */\n endDate?: string;\n /** Time bucket for aggregation (defaults to 'day'). */\n bucket?: BucketType;\n /** Aggregation operation to perform (defaults to 'sum'). */\n aggregation?: AggregationType;\n}\n\nexport interface AggregatedSample {\n /** ISO 8601 start date of the bucket. */\n startDate: string;\n /** ISO 8601 end date of the bucket. */\n endDate: string;\n /** Aggregated value for the bucket. */\n value: number;\n /** Unit of the aggregated value. */\n unit: HealthUnit;\n}\n\nexport interface QueryAggregatedResult {\n samples: AggregatedSample[];\n}\n\nexport interface HealthPlugin {\n /** Returns whether the current platform supports the native health SDK. */\n isAvailable(): Promise<AvailabilityResult>;\n /** Requests read/write access to the provided data types. */\n requestAuthorization(options: AuthorizationOptions): Promise<AuthorizationStatus>;\n /** Checks authorization status for the provided data types without prompting the user. */\n checkAuthorization(options: AuthorizationOptions): Promise<AuthorizationStatus>;\n /** Reads samples for the given data type within the specified time frame. */\n readSamples(options: QueryOptions): Promise<ReadSamplesResult>;\n /** Writes a single sample to the native health store. */\n saveSample(options: WriteSampleOptions): Promise<void>;\n\n /**\n * Get the native Capacitor plugin version\n *\n * @returns {Promise<{ version: string }>} a Promise with version for this device\n * @throws An error if something went wrong\n */\n getPluginVersion(): Promise<{ version: string }>;\n\n /**\n * Opens the Health Connect settings screen (Android only).\n * On iOS, this method does nothing.\n *\n * Use this to direct users to manage their Health Connect permissions\n * or to install Health Connect if not available.\n *\n * @throws An error if Health Connect settings cannot be opened\n */\n openHealthConnectSettings(): Promise<void>;\n\n /**\n * Shows the app's privacy policy for Health Connect (Android only).\n * On iOS, this method does nothing.\n *\n * This displays the same privacy policy screen that Health Connect shows\n * when the user taps \"Privacy policy\" in the permissions dialog.\n *\n * The privacy policy URL can be configured by adding a string resource\n * named \"health_connect_privacy_policy_url\" in your app's strings.xml,\n * or by placing an HTML file at www/privacypolicy.html in your assets.\n *\n * @throws An error if the privacy policy cannot be displayed\n */\n showPrivacyPolicy(): Promise<void>;\n\n /**\n * Queries workout sessions from the native health store.\n * Supported on iOS (HealthKit) and Android (Health Connect).\n *\n * @param options Query options including optional workout type filter, date range, limit, and sort order\n * @returns A promise that resolves with the workout sessions\n * @throws An error if something went wrong\n */\n queryWorkouts(options: QueryWorkoutsOptions): Promise<QueryWorkoutsResult>;\n\n /**\n * Queries aggregated health data from the native health store.\n * Aggregates data into time buckets (hour, day, week, month) with operations like sum, average, min, or max.\n * This is more efficient than fetching individual samples for large date ranges.\n *\n * Supported on iOS (HealthKit) and Android (Health Connect).\n *\n * @param options Query options including data type, date range, bucket size, and aggregation type\n * @returns A promise that resolves with the aggregated samples\n * @throws An error if something went wrong\n */\n queryAggregated(options: QueryAggregatedOptions): Promise<QueryAggregatedResult>;\n}\n"]}
@@ -160,6 +160,18 @@ enum HealthDataType: String, CaseIterable {
160
160
  case oxygenSaturation
161
161
  case restingHeartRate
162
162
  case heartRateVariability
163
+ case bloodPressure
164
+ case bloodGlucose
165
+ case bodyTemperature
166
+ case height
167
+ case flightsClimbed
168
+ case exerciseTime
169
+ case distanceCycling
170
+ case bodyFat
171
+ case basalBodyTemperature
172
+ case basalCalories
173
+ case totalCalories
174
+ case mindfulness
163
175
 
164
176
  func sampleType() throws -> HKSampleType {
165
177
  switch self {
@@ -168,6 +180,16 @@ enum HealthDataType: String, CaseIterable {
168
180
  throw HealthManagerError.dataTypeUnavailable(rawValue)
169
181
  }
170
182
  return type
183
+ case .bloodPressure:
184
+ guard let type = HKObjectType.correlationType(forIdentifier: .bloodPressure) else {
185
+ throw HealthManagerError.dataTypeUnavailable(rawValue)
186
+ }
187
+ return type
188
+ case .mindfulness:
189
+ guard let type = HKObjectType.categoryType(forIdentifier: .mindfulSession) else {
190
+ throw HealthManagerError.dataTypeUnavailable(rawValue)
191
+ }
192
+ return type
171
193
  default:
172
194
  return try quantityType()
173
195
  }
@@ -194,8 +216,32 @@ enum HealthDataType: String, CaseIterable {
194
216
  identifier = .restingHeartRate
195
217
  case .heartRateVariability:
196
218
  identifier = .heartRateVariabilitySDNN
219
+ case .bloodGlucose:
220
+ identifier = .bloodGlucose
221
+ case .bodyTemperature:
222
+ identifier = .bodyTemperature
223
+ case .height:
224
+ identifier = .height
225
+ case .flightsClimbed:
226
+ identifier = .flightsClimbed
227
+ case .exerciseTime:
228
+ identifier = .appleExerciseTime
229
+ case .distanceCycling:
230
+ identifier = .distanceCycling
231
+ case .bodyFat:
232
+ identifier = .bodyFatPercentage
233
+ case .basalBodyTemperature:
234
+ identifier = .basalBodyTemperature
235
+ case .basalCalories:
236
+ identifier = .basalEnergyBurned
237
+ case .totalCalories:
238
+ identifier = .activeEnergyBurned
197
239
  case .sleep:
198
240
  throw HealthManagerError.invalidDataType("Sleep is a category type, not a quantity type")
241
+ case .bloodPressure:
242
+ throw HealthManagerError.invalidDataType("Blood pressure is a correlation type, not a quantity type")
243
+ case .mindfulness:
244
+ throw HealthManagerError.invalidDataType("Mindfulness is a category type, not a quantity type")
199
245
  }
200
246
 
201
247
  guard let type = HKObjectType.quantityType(forIdentifier: identifier) else {
@@ -224,6 +270,28 @@ enum HealthDataType: String, CaseIterable {
224
270
  return HKUnit.secondUnit(with: .milli)
225
271
  case .sleep:
226
272
  return HKUnit.minute()
273
+ case .bloodPressure:
274
+ return HKUnit.millimeterOfMercury()
275
+ case .bloodGlucose:
276
+ return HKUnit.gramUnit(with: .milli).unitDivided(by: HKUnit.literUnit(with: .deci))
277
+ case .bodyTemperature, .basalBodyTemperature:
278
+ return HKUnit.degreeCelsius()
279
+ case .height:
280
+ return HKUnit.meterUnit(with: .centi)
281
+ case .flightsClimbed:
282
+ return HKUnit.count()
283
+ case .exerciseTime:
284
+ return HKUnit.minute()
285
+ case .distanceCycling:
286
+ return HKUnit.meter()
287
+ case .bodyFat:
288
+ return HKUnit.percent()
289
+ case .basalCalories:
290
+ return HKUnit.kilocalorie()
291
+ case .totalCalories:
292
+ return HKUnit.kilocalorie()
293
+ case .mindfulness:
294
+ return HKUnit.minute()
227
295
  }
228
296
  }
229
297
 
@@ -245,6 +313,28 @@ enum HealthDataType: String, CaseIterable {
245
313
  return "millisecond"
246
314
  case .sleep:
247
315
  return "minute"
316
+ case .bloodPressure:
317
+ return "mmHg"
318
+ case .bloodGlucose:
319
+ return "mg/dL"
320
+ case .bodyTemperature, .basalBodyTemperature:
321
+ return "celsius"
322
+ case .height:
323
+ return "centimeter"
324
+ case .flightsClimbed:
325
+ return "count"
326
+ case .exerciseTime:
327
+ return "minute"
328
+ case .distanceCycling:
329
+ return "meter"
330
+ case .bodyFat:
331
+ return "percent"
332
+ case .basalCalories:
333
+ return "kilocalorie"
334
+ case .totalCalories:
335
+ return "kilocalorie"
336
+ case .mindfulness:
337
+ return "minute"
248
338
  }
249
339
  }
250
340
 
@@ -415,6 +505,94 @@ final class Health {
415
505
  healthStore.execute(query)
416
506
  return
417
507
  }
508
+
509
+ // Handle mindfulness as a category sample
510
+ if dataType == .mindfulness {
511
+ let query = HKSampleQuery(sampleType: sampleType, predicate: predicate, limit: queryLimit, sortDescriptors: [sortDescriptor]) { [weak self] _, samples, error in
512
+ guard let self = self else { return }
513
+
514
+ if let error = error {
515
+ completion(.failure(error))
516
+ return
517
+ }
518
+
519
+ guard let categorySamples = samples as? [HKCategorySample] else {
520
+ completion(.success([]))
521
+ return
522
+ }
523
+
524
+ let results = categorySamples.map { sample -> [String: Any] in
525
+ let durationMinutes = sample.endDate.timeIntervalSince(sample.startDate) / 60.0
526
+
527
+ var payload: [String: Any] = [
528
+ "dataType": dataType.rawValue,
529
+ "value": durationMinutes,
530
+ "unit": dataType.unitIdentifier,
531
+ "startDate": self.isoFormatter.string(from: sample.startDate),
532
+ "endDate": self.isoFormatter.string(from: sample.endDate)
533
+ ]
534
+
535
+ let source = sample.sourceRevision.source
536
+ payload["sourceName"] = source.name
537
+ payload["sourceId"] = source.bundleIdentifier
538
+
539
+ return payload
540
+ }
541
+
542
+ completion(.success(results))
543
+ }
544
+ healthStore.execute(query)
545
+ return
546
+ }
547
+
548
+ // Handle blood pressure as a correlation sample
549
+ if dataType == .bloodPressure {
550
+ let query = HKSampleQuery(sampleType: sampleType, predicate: predicate, limit: queryLimit, sortDescriptors: [sortDescriptor]) { [weak self] _, samples, error in
551
+ guard let self = self else { return }
552
+
553
+ if let error = error {
554
+ completion(.failure(error))
555
+ return
556
+ }
557
+
558
+ guard let correlations = samples as? [HKCorrelation] else {
559
+ completion(.success([]))
560
+ return
561
+ }
562
+
563
+ let results = correlations.compactMap { correlation -> [String: Any]? in
564
+ guard let systolicType = HKObjectType.quantityType(forIdentifier: .bloodPressureSystolic),
565
+ let diastolicType = HKObjectType.quantityType(forIdentifier: .bloodPressureDiastolic),
566
+ let systolicSample = correlation.objects(for: systolicType).first as? HKQuantitySample,
567
+ let diastolicSample = correlation.objects(for: diastolicType).first as? HKQuantitySample else {
568
+ return nil
569
+ }
570
+
571
+ let systolicValue = systolicSample.quantity.doubleValue(for: HKUnit.millimeterOfMercury())
572
+ let diastolicValue = diastolicSample.quantity.doubleValue(for: HKUnit.millimeterOfMercury())
573
+
574
+ var payload: [String: Any] = [
575
+ "dataType": dataType.rawValue,
576
+ "value": systolicValue,
577
+ "unit": dataType.unitIdentifier,
578
+ "startDate": self.isoFormatter.string(from: correlation.startDate),
579
+ "endDate": self.isoFormatter.string(from: correlation.endDate),
580
+ "systolic": systolicValue,
581
+ "diastolic": diastolicValue
582
+ ]
583
+
584
+ let source = correlation.sourceRevision.source
585
+ payload["sourceName"] = source.name
586
+ payload["sourceId"] = source.bundleIdentifier
587
+
588
+ return payload
589
+ }
590
+
591
+ completion(.success(results))
592
+ }
593
+ healthStore.execute(query)
594
+ return
595
+ }
418
596
 
419
597
  // Handle quantity samples
420
598
  let query = HKSampleQuery(sampleType: sampleType, predicate: predicate, limit: queryLimit, sortDescriptors: [sortDescriptor]) { [weak self] _, samples, error in
@@ -481,7 +659,7 @@ final class Health {
481
659
  }
482
660
  }
483
661
 
484
- func saveSample(dataTypeIdentifier: String, value: Double, unitIdentifier: String?, startDateString: String?, endDateString: String?, metadata: [String: String]?, completion: @escaping (Result<Void, Error>) -> Void) throws {
662
+ func saveSample(dataTypeIdentifier: String, value: Double, unitIdentifier: String?, startDateString: String?, endDateString: String?, metadata: [String: String]?, systolic: Double?, diastolic: Double?, completion: @escaping (Result<Void, Error>) -> Void) throws {
485
663
  guard HKHealthStore.isHealthDataAvailable() else {
486
664
  throw HealthManagerError.healthDataUnavailable
487
665
  }
@@ -526,6 +704,63 @@ final class Health {
526
704
  }
527
705
  return
528
706
  }
707
+
708
+ // Handle mindfulness as a category sample
709
+ if dataType == .mindfulness {
710
+ guard let categoryType = HKObjectType.categoryType(forIdentifier: .mindfulSession) else {
711
+ throw HealthManagerError.dataTypeUnavailable(dataTypeIdentifier)
712
+ }
713
+ let sample = HKCategorySample(type: categoryType, value: 0, start: startDate, end: endDate, metadata: metadataDictionary)
714
+
715
+ healthStore.save(sample) { success, error in
716
+ if let error = error {
717
+ completion(.failure(error))
718
+ return
719
+ }
720
+
721
+ if success {
722
+ completion(.success(()))
723
+ } else {
724
+ completion(.failure(HealthManagerError.operationFailed("Failed to save the sample.")))
725
+ }
726
+ }
727
+ return
728
+ }
729
+
730
+ // Handle blood pressure as a correlation sample
731
+ if dataType == .bloodPressure {
732
+ guard let systolicValue = systolic, let diastolicValue = diastolic else {
733
+ throw HealthManagerError.operationFailed("Blood pressure requires both systolic and diastolic values")
734
+ }
735
+
736
+ guard let systolicType = HKObjectType.quantityType(forIdentifier: .bloodPressureSystolic),
737
+ let diastolicType = HKObjectType.quantityType(forIdentifier: .bloodPressureDiastolic),
738
+ let correlationType = HKObjectType.correlationType(forIdentifier: .bloodPressure) else {
739
+ throw HealthManagerError.dataTypeUnavailable(dataTypeIdentifier)
740
+ }
741
+
742
+ let systolicQuantity = HKQuantity(unit: HKUnit.millimeterOfMercury(), doubleValue: systolicValue)
743
+ let diastolicQuantity = HKQuantity(unit: HKUnit.millimeterOfMercury(), doubleValue: diastolicValue)
744
+
745
+ let systolicSample = HKQuantitySample(type: systolicType, quantity: systolicQuantity, start: startDate, end: endDate)
746
+ let diastolicSample = HKQuantitySample(type: diastolicType, quantity: diastolicQuantity, start: startDate, end: endDate)
747
+
748
+ let correlation = HKCorrelation(type: correlationType, start: startDate, end: endDate, objects: [systolicSample, diastolicSample], metadata: metadataDictionary)
749
+
750
+ healthStore.save(correlation) { success, error in
751
+ if let error = error {
752
+ completion(.failure(error))
753
+ return
754
+ }
755
+
756
+ if success {
757
+ completion(.success(()))
758
+ } else {
759
+ completion(.failure(HealthManagerError.operationFailed("Failed to save the sample.")))
760
+ }
761
+ }
762
+ return
763
+ }
529
764
 
530
765
  // Handle quantity samples
531
766
  let sampleType = try dataType.quantityType()
@@ -3,7 +3,7 @@ import Capacitor
3
3
 
4
4
  @objc(HealthPlugin)
5
5
  public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
6
- private let pluginVersion: String = "8.2.17"
6
+ private let pluginVersion: String = "8.2.18"
7
7
  public let identifier = "HealthPlugin"
8
8
  public let jsName = "Health"
9
9
  public let pluginMethods: [CAPPluginMethod] = [
@@ -110,6 +110,9 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
110
110
  result[entry.key] = stringValue
111
111
  }
112
112
  }
113
+
114
+ let systolic = call.getDouble("systolic")
115
+ let diastolic = call.getDouble("diastolic")
113
116
 
114
117
  do {
115
118
  try implementation.saveSample(
@@ -118,7 +121,9 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
118
121
  unitIdentifier: unit,
119
122
  startDateString: startDate,
120
123
  endDateString: endDate,
121
- metadata: metadata
124
+ metadata: metadata,
125
+ systolic: systolic,
126
+ diastolic: diastolic
122
127
  ) { result in
123
128
  DispatchQueue.main.async {
124
129
  switch result {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-health",
3
- "version": "8.2.17",
3
+ "version": "8.2.18",
4
4
  "description": "Capacitor plugin to interact with data from Apple HealthKit and Health Connect",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",