@flomentumsolutions/capacitor-health-extended 0.7.1 → 0.7.3
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 +47 -6
- package/android/build.gradle +1 -1
- package/android/src/main/AndroidManifest.xml +1 -1
- package/android/src/main/java/com/{flomentum/vitals/capacitor → flomentumsolutions/capacitor-health-extended}/HealthPlugin.kt +117 -8
- package/dist/esm/definitions.d.ts +15 -1
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/HealthPluginPlugin/HealthPlugin.swift +98 -5
- package/package.json +1 -1
- /package/android/src/main/java/com/{flomentum/vitals/capacitor → flomentumsolutions/capacitor-health-extended}/PermissionsRationaleActivity.kt +0 -0
package/README.md
CHANGED
|
@@ -28,6 +28,7 @@ Thanks [@mley](https://github.com/mley) for the ground work. The goal of this fo
|
|
|
28
28
|
- Query aggregated data like steps or calories
|
|
29
29
|
- Retrieve workout sessions with optional route and heart rate data
|
|
30
30
|
- Create workout sessions (e.g., rock climbing) with totals, optional routes, and heart-rate samples (write APIs)
|
|
31
|
+
- Save manual metrics (weight, height, body fat %, resting heart rate) to HealthKit/Health Connect (write APIs)
|
|
31
32
|
- Fetch the latest samples for steps, distance (incl. cycling), calories (active/total/basal), heart‑rate, resting HR, HRV, respiratory rate, blood pressure, oxygen saturation, blood glucose, body temperature (basal + core), body fat, height, weight, flights climbed, sleep (incl. REM duration), and exercise time.
|
|
32
33
|
- Read profile characteristics on iOS: biological sex, blood type, date of birth, Fitzpatrick skin type, wheelchair use.
|
|
33
34
|
|
|
@@ -58,10 +59,12 @@ you can keep using the CocoaPods spec `FlomentumSolutionsCapacitorHealthExtended
|
|
|
58
59
|
* Make sure your app id has the 'HealthKit' entitlement when this plugin is installed (see iOS dev center).
|
|
59
60
|
* Also, make sure your app and App Store description comply with the Apple review guidelines.
|
|
60
61
|
* There are two keys to be added to the info.plist file: NSHealthShareUsageDescription and NSHealthUpdateUsageDescription.
|
|
61
|
-
* Request WRITE_* permissions with `requestHealthPermissions` to enable saving workouts/energy/distance/routes/heart-rate samples to HealthKit.
|
|
62
|
+
* Request WRITE_* permissions with `requestHealthPermissions` to enable saving workouts/energy/distance/routes/heart-rate samples and manual metrics (weight/height/body fat/resting HR) to HealthKit.
|
|
62
63
|
|
|
63
64
|
### Android
|
|
64
65
|
|
|
66
|
+
The plugin namespace/package is `com.flomentumsolutions.capacitorhealthextended.capacitor` (hyphen removed from older `com.flomentumsolutions.capacitor-health-extended` references); use this when wiring activities, manifest entries, or ProGuard rules.
|
|
67
|
+
|
|
65
68
|
* Android Manifest in root tag right after opening manifest tag
|
|
66
69
|
```xml
|
|
67
70
|
<!-- Make Health Connect visible to detect installation -->
|
|
@@ -100,9 +103,13 @@ you can keep using the CocoaPods spec `FlomentumSolutionsCapacitorHealthExtended
|
|
|
100
103
|
<uses-permission android:name="android.permission.health.WRITE_DISTANCE" />
|
|
101
104
|
<uses-permission android:name="android.permission.health.WRITE_HEART_RATE" />
|
|
102
105
|
<uses-permission android:name="android.permission.health.WRITE_EXERCISE_ROUTE" />
|
|
106
|
+
<uses-permission android:name="android.permission.health.WRITE_WEIGHT" />
|
|
107
|
+
<uses-permission android:name="android.permission.health.WRITE_HEIGHT" />
|
|
108
|
+
<uses-permission android:name="android.permission.health.WRITE_BODY_FAT" />
|
|
109
|
+
<uses-permission android:name="android.permission.health.WRITE_RESTING_HEART_RATE" />
|
|
103
110
|
```
|
|
104
111
|
|
|
105
|
-
Include the WRITE_* entries when you call `saveWorkout` to insert exercise sessions, energy, distance, routes, or heart rate samples
|
|
112
|
+
Include the WRITE_* entries when you call `saveWorkout` to insert exercise sessions, energy, distance, routes, or heart rate samples, and add the metric write permissions when using `saveMetrics`.
|
|
106
113
|
|
|
107
114
|
* Android Manifest in application tag
|
|
108
115
|
```xml
|
|
@@ -239,6 +246,7 @@ This setup ensures your WebView will load HTTPS content securely and complies wi
|
|
|
239
246
|
* [`queryHeartRate()`](#queryheartrate)
|
|
240
247
|
* [`querySteps()`](#querysteps)
|
|
241
248
|
* [`saveWorkout(...)`](#saveworkout)
|
|
249
|
+
* [`saveMetrics(...)`](#savemetrics)
|
|
242
250
|
* [Interfaces](#interfaces)
|
|
243
251
|
* [Type Aliases](#type-aliases)
|
|
244
252
|
|
|
@@ -483,6 +491,23 @@ Create a workout session with optional totals and route/heart-rate samples.
|
|
|
483
491
|
--------------------
|
|
484
492
|
|
|
485
493
|
|
|
494
|
+
### saveMetrics(...)
|
|
495
|
+
|
|
496
|
+
```typescript
|
|
497
|
+
saveMetrics(request: SaveMetricsRequest) => Promise<SaveMetricsResponse>
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
Save user-provided body metrics to the health platform.
|
|
501
|
+
|
|
502
|
+
| Param | Type |
|
|
503
|
+
| ------------- | ----------------------------------------------------------------- |
|
|
504
|
+
| **`request`** | <code><a href="#savemetricsrequest">SaveMetricsRequest</a></code> |
|
|
505
|
+
|
|
506
|
+
**Returns:** <code>Promise<<a href="#savemetricsresponse">SaveMetricsResponse</a>></code>
|
|
507
|
+
|
|
508
|
+
--------------------
|
|
509
|
+
|
|
510
|
+
|
|
486
511
|
### Interfaces
|
|
487
512
|
|
|
488
513
|
|
|
@@ -631,6 +656,24 @@ Create a workout session with optional totals and route/heart-rate samples.
|
|
|
631
656
|
| **`heartRateSamples`** | <code>HeartRateSample[]</code> |
|
|
632
657
|
|
|
633
658
|
|
|
659
|
+
#### SaveMetricsResponse
|
|
660
|
+
|
|
661
|
+
| Prop | Type |
|
|
662
|
+
| -------------- | -------------------- |
|
|
663
|
+
| **`success`** | <code>boolean</code> |
|
|
664
|
+
| **`inserted`** | <code>number</code> |
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
#### SaveMetricsRequest
|
|
668
|
+
|
|
669
|
+
| Prop | Type |
|
|
670
|
+
| ---------------------- | ------------------- |
|
|
671
|
+
| **`weightKg`** | <code>number</code> |
|
|
672
|
+
| **`heightCm`** | <code>number</code> |
|
|
673
|
+
| **`bodyFatPercent`** | <code>number</code> |
|
|
674
|
+
| **`restingHeartRate`** | <code>number</code> |
|
|
675
|
+
|
|
676
|
+
|
|
634
677
|
### Type Aliases
|
|
635
678
|
|
|
636
679
|
|
|
@@ -638,14 +681,12 @@ Create a workout session with optional totals and route/heart-rate samples.
|
|
|
638
681
|
|
|
639
682
|
Construct a type with a set of properties K of type T
|
|
640
683
|
|
|
641
|
-
<code>{
|
|
642
|
-
[P in K]: T;
|
|
643
|
-
}</code>
|
|
684
|
+
<code>{
|
|
644
685
|
[P in K]: T;
|
|
645
686
|
}</code>
|
|
646
687
|
|
|
647
688
|
|
|
648
689
|
#### HealthPermission
|
|
649
690
|
|
|
650
|
-
<code>'READ_STEPS' | 'READ_WORKOUTS' | 'WRITE_WORKOUTS' | 'READ_ACTIVE_CALORIES' | 'WRITE_ACTIVE_CALORIES' | 'READ_TOTAL_CALORIES' | 'WRITE_TOTAL_CALORIES' | 'READ_DISTANCE' | 'WRITE_DISTANCE' | 'READ_WEIGHT' | 'READ_HEIGHT' | 'READ_HEART_RATE' | 'WRITE_HEART_RATE' | 'READ_RESTING_HEART_RATE' | 'READ_ROUTE' | 'WRITE_ROUTE' | 'READ_MINDFULNESS' | 'READ_HRV' | 'READ_BLOOD_PRESSURE' | 'READ_BASAL_CALORIES' | 'READ_RESPIRATORY_RATE' | 'READ_OXYGEN_SATURATION' | 'READ_BLOOD_GLUCOSE' | 'READ_BODY_TEMPERATURE' | 'READ_BASAL_BODY_TEMPERATURE' | 'READ_BODY_FAT' | 'READ_FLOORS_CLIMBED' | 'READ_SLEEP' | 'READ_EXERCISE_TIME' | 'READ_BIOLOGICAL_SEX' | 'READ_BLOOD_TYPE' | 'READ_DATE_OF_BIRTH' | 'READ_FITZPATRICK_SKIN_TYPE' | 'READ_WHEELCHAIR_USE'</code>
|
|
691
|
+
<code>'READ_STEPS' | 'READ_WORKOUTS' | 'WRITE_WORKOUTS' | 'READ_ACTIVE_CALORIES' | 'WRITE_ACTIVE_CALORIES' | 'READ_TOTAL_CALORIES' | 'WRITE_TOTAL_CALORIES' | 'READ_DISTANCE' | 'WRITE_DISTANCE' | 'READ_WEIGHT' | 'WRITE_WEIGHT' | 'READ_HEIGHT' | 'WRITE_HEIGHT' | 'READ_HEART_RATE' | 'WRITE_HEART_RATE' | 'READ_RESTING_HEART_RATE' | 'WRITE_RESTING_HEART_RATE' | 'READ_ROUTE' | 'WRITE_ROUTE' | 'READ_MINDFULNESS' | 'READ_HRV' | 'READ_BLOOD_PRESSURE' | 'READ_BASAL_CALORIES' | 'READ_RESPIRATORY_RATE' | 'READ_OXYGEN_SATURATION' | 'READ_BLOOD_GLUCOSE' | 'READ_BODY_TEMPERATURE' | 'READ_BASAL_BODY_TEMPERATURE' | 'READ_BODY_FAT' | 'WRITE_BODY_FAT' | 'READ_FLOORS_CLIMBED' | 'READ_SLEEP' | 'READ_EXERCISE_TIME' | 'READ_BIOLOGICAL_SEX' | 'READ_BLOOD_TYPE' | 'READ_DATE_OF_BIRTH' | 'READ_FITZPATRICK_SKIN_TYPE' | 'READ_WHEELCHAIR_USE'</code>
|
|
651
692
|
|
|
652
693
|
|
|
653
694
|
#### HealthBiologicalSex
|
package/android/build.gradle
CHANGED
|
@@ -25,7 +25,7 @@ apply plugin: 'com.android.library'
|
|
|
25
25
|
apply plugin: 'org.jetbrains.kotlin.android'
|
|
26
26
|
|
|
27
27
|
android {
|
|
28
|
-
namespace = "com.flomentumsolutions.
|
|
28
|
+
namespace = "com.flomentumsolutions.capacitorhealthextended.capacitor"
|
|
29
29
|
compileSdk = project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 36
|
|
30
30
|
defaultConfig {
|
|
31
31
|
minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 36
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.flomentumsolutions.
|
|
1
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.flomentumsolutions.capacitorhealthextended.capacitor">
|
|
2
2
|
|
|
3
3
|
</manifest>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
package com.flomentumsolutions.
|
|
1
|
+
package com.flomentumsolutions.capacitorhealthextended.capacitor
|
|
2
2
|
|
|
3
3
|
import android.content.Intent
|
|
4
4
|
import android.util.Log
|
|
@@ -17,7 +17,9 @@ import androidx.health.connect.client.request.ReadRecordsRequest
|
|
|
17
17
|
import androidx.health.connect.client.time.TimeRangeFilter
|
|
18
18
|
import androidx.health.connect.client.records.Record
|
|
19
19
|
import androidx.health.connect.client.units.kilocalories
|
|
20
|
+
import androidx.health.connect.client.units.kilograms
|
|
20
21
|
import androidx.health.connect.client.units.meters
|
|
22
|
+
import androidx.health.connect.client.units.percent
|
|
21
23
|
import com.getcapacitor.JSArray
|
|
22
24
|
import com.getcapacitor.JSObject
|
|
23
25
|
import com.getcapacitor.Plugin
|
|
@@ -66,7 +68,11 @@ enum class CapHealthPermission {
|
|
|
66
68
|
WRITE_TOTAL_CALORIES,
|
|
67
69
|
WRITE_DISTANCE,
|
|
68
70
|
WRITE_HEART_RATE,
|
|
69
|
-
WRITE_ROUTE
|
|
71
|
+
WRITE_ROUTE,
|
|
72
|
+
WRITE_WEIGHT,
|
|
73
|
+
WRITE_HEIGHT,
|
|
74
|
+
WRITE_BODY_FAT,
|
|
75
|
+
WRITE_RESTING_HEART_RATE;
|
|
70
76
|
|
|
71
77
|
companion object {
|
|
72
78
|
fun from(s: String): CapHealthPermission? {
|
|
@@ -110,7 +116,11 @@ enum class CapHealthPermission {
|
|
|
110
116
|
Permission(alias = "WRITE_TOTAL_CALORIES", strings = ["android.permission.health.WRITE_TOTAL_CALORIES_BURNED"]),
|
|
111
117
|
Permission(alias = "WRITE_DISTANCE", strings = ["android.permission.health.WRITE_DISTANCE"]),
|
|
112
118
|
Permission(alias = "WRITE_HEART_RATE", strings = ["android.permission.health.WRITE_HEART_RATE"]),
|
|
113
|
-
Permission(alias = "WRITE_ROUTE", strings = ["android.permission.health.WRITE_EXERCISE_ROUTE"])
|
|
119
|
+
Permission(alias = "WRITE_ROUTE", strings = ["android.permission.health.WRITE_EXERCISE_ROUTE"]),
|
|
120
|
+
Permission(alias = "WRITE_WEIGHT", strings = ["android.permission.health.WRITE_WEIGHT"]),
|
|
121
|
+
Permission(alias = "WRITE_HEIGHT", strings = ["android.permission.health.WRITE_HEIGHT"]),
|
|
122
|
+
Permission(alias = "WRITE_BODY_FAT", strings = ["android.permission.health.WRITE_BODY_FAT"]),
|
|
123
|
+
Permission(alias = "WRITE_RESTING_HEART_RATE", strings = ["android.permission.health.WRITE_RESTING_HEART_RATE"])
|
|
114
124
|
]
|
|
115
125
|
)
|
|
116
126
|
|
|
@@ -153,7 +163,11 @@ class HealthPlugin : Plugin() {
|
|
|
153
163
|
CapHealthPermission.WRITE_TOTAL_CALORIES to HealthPermission.getWritePermission(TotalCaloriesBurnedRecord::class),
|
|
154
164
|
CapHealthPermission.WRITE_DISTANCE to HealthPermission.getWritePermission(DistanceRecord::class),
|
|
155
165
|
CapHealthPermission.WRITE_HEART_RATE to HealthPermission.getWritePermission(HeartRateRecord::class),
|
|
156
|
-
CapHealthPermission.WRITE_ROUTE to HealthPermission.PERMISSION_WRITE_EXERCISE_ROUTE
|
|
166
|
+
CapHealthPermission.WRITE_ROUTE to HealthPermission.PERMISSION_WRITE_EXERCISE_ROUTE,
|
|
167
|
+
CapHealthPermission.WRITE_WEIGHT to HealthPermission.getWritePermission(WeightRecord::class),
|
|
168
|
+
CapHealthPermission.WRITE_HEIGHT to HealthPermission.getWritePermission(HeightRecord::class),
|
|
169
|
+
CapHealthPermission.WRITE_BODY_FAT to HealthPermission.getWritePermission(BodyFatRecord::class),
|
|
170
|
+
CapHealthPermission.WRITE_RESTING_HEART_RATE to HealthPermission.getWritePermission(RestingHeartRateRecord::class)
|
|
157
171
|
)
|
|
158
172
|
|
|
159
173
|
override fun load() {
|
|
@@ -1519,6 +1533,101 @@ class HealthPlugin : Plugin() {
|
|
|
1519
1533
|
return granted.contains(targetPermission)
|
|
1520
1534
|
}
|
|
1521
1535
|
|
|
1536
|
+
@PluginMethod
|
|
1537
|
+
fun saveMetrics(call: PluginCall) {
|
|
1538
|
+
if (!ensureClientInitialized(call)) return
|
|
1539
|
+
|
|
1540
|
+
val weightKg = call.getDouble("weightKg")
|
|
1541
|
+
val heightCm = call.getDouble("heightCm")
|
|
1542
|
+
val bodyFatPercent = call.getDouble("bodyFatPercent")
|
|
1543
|
+
val restingHeartRate = call.getDouble("restingHeartRate")
|
|
1544
|
+
|
|
1545
|
+
if (weightKg == null && heightCm == null && bodyFatPercent == null && restingHeartRate == null) {
|
|
1546
|
+
call.reject("No metrics provided")
|
|
1547
|
+
return
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
CoroutineScope(Dispatchers.IO).launch {
|
|
1551
|
+
try {
|
|
1552
|
+
val granted = healthConnectClient.permissionController.getGrantedPermissions()
|
|
1553
|
+
fun ensurePermission(cap: CapHealthPermission, name: String): Boolean {
|
|
1554
|
+
val hc = permissionMapping[cap]
|
|
1555
|
+
val ok = hc != null && granted.contains(hc)
|
|
1556
|
+
if (!ok) {
|
|
1557
|
+
call.reject("Missing $name permission")
|
|
1558
|
+
}
|
|
1559
|
+
return ok
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
val now = Instant.now()
|
|
1563
|
+
val offset = now.atZone(ZoneId.systemDefault()).offset
|
|
1564
|
+
|
|
1565
|
+
val records = mutableListOf<Record>()
|
|
1566
|
+
|
|
1567
|
+
weightKg?.let {
|
|
1568
|
+
if (!ensurePermission(CapHealthPermission.WRITE_WEIGHT, "WRITE_WEIGHT")) return@launch
|
|
1569
|
+
records.add(
|
|
1570
|
+
WeightRecord(
|
|
1571
|
+
time = now,
|
|
1572
|
+
zoneOffset = offset,
|
|
1573
|
+
weight = it.kilograms,
|
|
1574
|
+
metadata = Metadata.manualEntry()
|
|
1575
|
+
)
|
|
1576
|
+
)
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
heightCm?.let {
|
|
1580
|
+
if (!ensurePermission(CapHealthPermission.WRITE_HEIGHT, "WRITE_HEIGHT")) return@launch
|
|
1581
|
+
records.add(
|
|
1582
|
+
HeightRecord(
|
|
1583
|
+
time = now,
|
|
1584
|
+
zoneOffset = offset,
|
|
1585
|
+
height = (it / 100.0).meters,
|
|
1586
|
+
metadata = Metadata.manualEntry()
|
|
1587
|
+
)
|
|
1588
|
+
)
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
bodyFatPercent?.let {
|
|
1592
|
+
if (!ensurePermission(CapHealthPermission.WRITE_BODY_FAT, "WRITE_BODY_FAT")) return@launch
|
|
1593
|
+
records.add(
|
|
1594
|
+
BodyFatRecord(
|
|
1595
|
+
time = now,
|
|
1596
|
+
zoneOffset = offset,
|
|
1597
|
+
percentage = it.percent,
|
|
1598
|
+
metadata = Metadata.manualEntry()
|
|
1599
|
+
)
|
|
1600
|
+
)
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
restingHeartRate?.let {
|
|
1604
|
+
if (!ensurePermission(CapHealthPermission.WRITE_RESTING_HEART_RATE, "WRITE_RESTING_HEART_RATE")) return@launch
|
|
1605
|
+
records.add(
|
|
1606
|
+
RestingHeartRateRecord(
|
|
1607
|
+
time = now,
|
|
1608
|
+
zoneOffset = offset,
|
|
1609
|
+
beatsPerMinute = it.toLong(),
|
|
1610
|
+
metadata = Metadata.manualEntry()
|
|
1611
|
+
)
|
|
1612
|
+
)
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
if (records.isEmpty()) {
|
|
1616
|
+
call.reject("No metrics were added; missing permissions?")
|
|
1617
|
+
return@launch
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
val response = healthConnectClient.insertRecords(records)
|
|
1621
|
+
val result = JSObject()
|
|
1622
|
+
result.put("success", true)
|
|
1623
|
+
result.put("inserted", response.recordIdsList.size)
|
|
1624
|
+
call.resolve(result)
|
|
1625
|
+
} catch (e: Exception) {
|
|
1626
|
+
call.reject("Failed to save metrics: ${e.message}")
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1522
1631
|
@PluginMethod
|
|
1523
1632
|
fun saveWorkout(call: PluginCall) {
|
|
1524
1633
|
if (!ensureClientInitialized(call)) return
|
|
@@ -1600,7 +1709,7 @@ class HealthPlugin : Plugin() {
|
|
|
1600
1709
|
startZoneOffset = startZoneOffset,
|
|
1601
1710
|
endTime = endInstant,
|
|
1602
1711
|
endZoneOffset = endZoneOffset,
|
|
1603
|
-
energy = kilocalories
|
|
1712
|
+
energy = calories.kilocalories,
|
|
1604
1713
|
metadata = Metadata.manualEntry()
|
|
1605
1714
|
)
|
|
1606
1715
|
)
|
|
@@ -1612,7 +1721,7 @@ class HealthPlugin : Plugin() {
|
|
|
1612
1721
|
startZoneOffset = startZoneOffset,
|
|
1613
1722
|
endTime = endInstant,
|
|
1614
1723
|
endZoneOffset = endZoneOffset,
|
|
1615
|
-
energy = kilocalories
|
|
1724
|
+
energy = calories.kilocalories,
|
|
1616
1725
|
metadata = Metadata.manualEntry()
|
|
1617
1726
|
)
|
|
1618
1727
|
)
|
|
@@ -1632,7 +1741,7 @@ class HealthPlugin : Plugin() {
|
|
|
1632
1741
|
startZoneOffset = startZoneOffset,
|
|
1633
1742
|
endTime = endInstant,
|
|
1634
1743
|
endZoneOffset = endZoneOffset,
|
|
1635
|
-
distance = meters
|
|
1744
|
+
distance = distance.meters,
|
|
1636
1745
|
metadata = Metadata.manualEntry()
|
|
1637
1746
|
)
|
|
1638
1747
|
)
|
|
@@ -1890,7 +1999,7 @@ class HealthPlugin : Plugin() {
|
|
|
1890
1999
|
time = ts,
|
|
1891
2000
|
latitude = lat,
|
|
1892
2001
|
longitude = lng,
|
|
1893
|
-
altitude = if (altitudeValue.isNaN()) null else meters
|
|
2002
|
+
altitude = if (altitudeValue.isNaN()) null else altitudeValue.meters
|
|
1894
2003
|
)
|
|
1895
2004
|
}
|
|
1896
2005
|
?.sortedBy { it.time }
|
|
@@ -93,8 +93,12 @@ export interface HealthPlugin {
|
|
|
93
93
|
* - Requires matching WRITE_* permissions for the values you include (e.g., WRITE_WORKOUTS + WRITE_ACTIVE_CALORIES + WRITE_DISTANCE + WRITE_HEART_RATE + WRITE_ROUTE).
|
|
94
94
|
*/
|
|
95
95
|
saveWorkout(request: SaveWorkoutRequest): Promise<SaveWorkoutResponse>;
|
|
96
|
+
/**
|
|
97
|
+
* Save user-provided body metrics to the health platform.
|
|
98
|
+
*/
|
|
99
|
+
saveMetrics(request: SaveMetricsRequest): Promise<SaveMetricsResponse>;
|
|
96
100
|
}
|
|
97
|
-
export declare type HealthPermission = 'READ_STEPS' | 'READ_WORKOUTS' | 'WRITE_WORKOUTS' | 'READ_ACTIVE_CALORIES' | 'WRITE_ACTIVE_CALORIES' | 'READ_TOTAL_CALORIES' | 'WRITE_TOTAL_CALORIES' | 'READ_DISTANCE' | 'WRITE_DISTANCE' | 'READ_WEIGHT' | 'READ_HEIGHT' | 'READ_HEART_RATE' | 'WRITE_HEART_RATE' | 'READ_RESTING_HEART_RATE' | 'READ_ROUTE' | 'WRITE_ROUTE' | 'READ_MINDFULNESS' | 'READ_HRV' | 'READ_BLOOD_PRESSURE' | 'READ_BASAL_CALORIES' | 'READ_RESPIRATORY_RATE' | 'READ_OXYGEN_SATURATION' | 'READ_BLOOD_GLUCOSE' | 'READ_BODY_TEMPERATURE' | 'READ_BASAL_BODY_TEMPERATURE' | 'READ_BODY_FAT' | 'READ_FLOORS_CLIMBED' | 'READ_SLEEP' | 'READ_EXERCISE_TIME' | 'READ_BIOLOGICAL_SEX' | 'READ_BLOOD_TYPE' | 'READ_DATE_OF_BIRTH' | 'READ_FITZPATRICK_SKIN_TYPE' | 'READ_WHEELCHAIR_USE';
|
|
101
|
+
export declare type HealthPermission = 'READ_STEPS' | 'READ_WORKOUTS' | 'WRITE_WORKOUTS' | 'READ_ACTIVE_CALORIES' | 'WRITE_ACTIVE_CALORIES' | 'READ_TOTAL_CALORIES' | 'WRITE_TOTAL_CALORIES' | 'READ_DISTANCE' | 'WRITE_DISTANCE' | 'READ_WEIGHT' | 'WRITE_WEIGHT' | 'READ_HEIGHT' | 'WRITE_HEIGHT' | 'READ_HEART_RATE' | 'WRITE_HEART_RATE' | 'READ_RESTING_HEART_RATE' | 'WRITE_RESTING_HEART_RATE' | 'READ_ROUTE' | 'WRITE_ROUTE' | 'READ_MINDFULNESS' | 'READ_HRV' | 'READ_BLOOD_PRESSURE' | 'READ_BASAL_CALORIES' | 'READ_RESPIRATORY_RATE' | 'READ_OXYGEN_SATURATION' | 'READ_BLOOD_GLUCOSE' | 'READ_BODY_TEMPERATURE' | 'READ_BASAL_BODY_TEMPERATURE' | 'READ_BODY_FAT' | 'WRITE_BODY_FAT' | 'READ_FLOORS_CLIMBED' | 'READ_SLEEP' | 'READ_EXERCISE_TIME' | 'READ_BIOLOGICAL_SEX' | 'READ_BLOOD_TYPE' | 'READ_DATE_OF_BIRTH' | 'READ_FITZPATRICK_SKIN_TYPE' | 'READ_WHEELCHAIR_USE';
|
|
98
102
|
export type LatestDataType = 'steps' | 'active-calories' | 'total-calories' | 'basal-calories' | 'distance' | 'weight' | 'height' | 'heart-rate' | 'resting-heart-rate' | 'respiratory-rate' | 'oxygen-saturation' | 'blood-glucose' | 'body-temperature' | 'basal-body-temperature' | 'body-fat' | 'flights-climbed' | 'exercise-time' | 'distance-cycling' | 'mindfulness' | 'sleep' | 'sleep-rem' | 'hrv' | 'blood-pressure';
|
|
99
103
|
export interface PermissionsRequest {
|
|
100
104
|
permissions: HealthPermission[];
|
|
@@ -137,6 +141,16 @@ export interface SaveWorkoutResponse {
|
|
|
137
141
|
success: boolean;
|
|
138
142
|
id?: string;
|
|
139
143
|
}
|
|
144
|
+
export interface SaveMetricsRequest {
|
|
145
|
+
weightKg?: number;
|
|
146
|
+
heightCm?: number;
|
|
147
|
+
bodyFatPercent?: number;
|
|
148
|
+
restingHeartRate?: number;
|
|
149
|
+
}
|
|
150
|
+
export interface SaveMetricsResponse {
|
|
151
|
+
success: boolean;
|
|
152
|
+
inserted?: number;
|
|
153
|
+
}
|
|
140
154
|
export interface Workout {
|
|
141
155
|
startDate: string;
|
|
142
156
|
endDate: string;
|
|
@@ -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 * iOS only: Reads user characteristics such as biological sex, blood type, date of birth, Fitzpatrick skin type, and wheelchair use.\n * Values are null when unavailable or permission was not granted. Android does not expose these characteristics; it returns `platformSupported: false` and a `platformMessage` for UI hints without emitting null values.\n */\n getCharacteristics(): Promise<CharacteristicsResponse>;\n\n /**\n * Query aggregated data\n * - Blood-pressure aggregates return the systolic average in `value` plus `systolic`, `diastolic`, and `unit`.\n * - `total-calories` is derived as active + basal energy on both iOS and Android for latest samples, aggregated queries, and workouts. We fall back to the platform's total‑calories metric (or active calories) when basal data isn't available or permission is missing. Request both `READ_ACTIVE_CALORIES` and `READ_BASAL_CALORIES` for full totals.\n * - Weight/height aggregation returns the latest sample per day (no averaging).\n * - Android aggregation currently supports daily buckets; unsupported buckets will be rejected.\n * - Android `distance-cycling` aggregates distance recorded during biking exercise sessions (requires distance + workouts permissions).\n * - Daily `bucket: \"day\"` queries use calendar-day boundaries in the device time zone (start-of-day through the next start-of-day) instead of a trailing 24-hour window. For “today,” send `startDate` at today’s start-of-day and `endDate` at now or tomorrow’s start-of-day.\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 /**\n * Query latest sample for a specific data type\n * - Latest sleep sample returns the most recent complete sleep session (asleep states only) from the last ~36 hours; if a longer overnight session exists, shorter naps are ignored.\n * - `sleep-rem` returns REM duration (minutes) for the latest sleep session; requires iOS 16+ sleep stages and Health Connect REM data on Android.\n * @param request\n */\n queryLatestSample(request: { dataType: LatestDataType }): Promise<QueryLatestSampleResponse>;\n\n /**\n * Query latest weight sample\n */\n queryWeight(): Promise<QueryLatestSampleResponse>;\n\n /**\n * Query latest height sample\n */\n queryHeight(): Promise<QueryLatestSampleResponse>;\n\n /**\n * Query latest heart rate sample\n */\n queryHeartRate(): Promise<QueryLatestSampleResponse>;\n\n /**\n * Query latest steps sample\n */\n querySteps(): Promise<QueryLatestSampleResponse>;\n\n /**\n * Create a workout session with optional totals and route/heart-rate samples.\n * - iOS stores an `HKWorkout` (activityType mapped from `activityType`) with total energy/distance and optional metadata/route/heart-rate samples.\n * - Android stores an `ExerciseSessionRecord` plus `ActiveCaloriesBurnedRecord`, `DistanceRecord`, and `HeartRateRecord` when provided. Routes are attached via `ExerciseRoute`.\n * - Requires matching WRITE_* permissions for the values you include (e.g., WRITE_WORKOUTS + WRITE_ACTIVE_CALORIES + WRITE_DISTANCE + WRITE_HEART_RATE + WRITE_ROUTE).\n */\n saveWorkout(request: SaveWorkoutRequest): Promise<SaveWorkoutResponse>;\n}\n\nexport declare type HealthPermission =\n | 'READ_STEPS'\n | 'READ_WORKOUTS'\n | 'WRITE_WORKOUTS'\n | 'READ_ACTIVE_CALORIES'\n | 'WRITE_ACTIVE_CALORIES'\n | 'READ_TOTAL_CALORIES'\n | 'WRITE_TOTAL_CALORIES'\n | 'READ_DISTANCE'\n | 'WRITE_DISTANCE'\n | 'READ_WEIGHT'\n | 'READ_HEIGHT'\n | 'READ_HEART_RATE'\n | 'WRITE_HEART_RATE'\n | 'READ_RESTING_HEART_RATE'\n | 'READ_ROUTE'\n | 'WRITE_ROUTE'\n | 'READ_MINDFULNESS'\n | 'READ_HRV'\n | 'READ_BLOOD_PRESSURE'\n | 'READ_BASAL_CALORIES'\n | 'READ_RESPIRATORY_RATE'\n | 'READ_OXYGEN_SATURATION'\n | 'READ_BLOOD_GLUCOSE'\n | 'READ_BODY_TEMPERATURE'\n | 'READ_BASAL_BODY_TEMPERATURE'\n | 'READ_BODY_FAT'\n | 'READ_FLOORS_CLIMBED'\n | 'READ_SLEEP'\n | 'READ_EXERCISE_TIME'\n | 'READ_BIOLOGICAL_SEX'\n | 'READ_BLOOD_TYPE'\n | 'READ_DATE_OF_BIRTH'\n | 'READ_FITZPATRICK_SKIN_TYPE'\n | 'READ_WHEELCHAIR_USE';\n\nexport type LatestDataType =\n | 'steps'\n | 'active-calories'\n | 'total-calories'\n | 'basal-calories'\n | 'distance'\n | 'weight'\n | 'height'\n | 'heart-rate'\n | 'resting-heart-rate'\n | 'respiratory-rate'\n | 'oxygen-saturation'\n | 'blood-glucose'\n | 'body-temperature'\n | 'basal-body-temperature'\n | 'body-fat'\n | 'flights-climbed'\n | 'exercise-time'\n | 'distance-cycling'\n | 'mindfulness'\n | 'sleep'\n | 'sleep-rem'\n | 'hrv'\n | 'blood-pressure';\n\nexport interface PermissionsRequest {\n permissions: HealthPermission[];\n}\n\nexport interface PermissionResponse {\n permissions: Record<HealthPermission, 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 type WorkoutActivityType =\n | 'rock-climbing'\n | 'climbing'\n | 'hiking'\n | 'running'\n | 'walking'\n | 'cycling'\n | 'biking'\n | 'strength-training'\n | 'yoga'\n | 'other';\n\nexport interface SaveWorkoutRequest {\n activityType: WorkoutActivityType;\n startDate: string;\n endDate: string;\n calories?: number;\n distance?: number;\n metadata?: Record<string, any>;\n route?: RouteSample[];\n heartRateSamples?: HeartRateSample[];\n}\n\nexport interface SaveWorkoutResponse {\n success: boolean;\n id?: string;\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:\n | 'steps'\n | 'active-calories'\n | 'total-calories'\n | 'basal-calories'\n | 'distance'\n | 'weight'\n | 'height'\n | 'heart-rate'\n | 'resting-heart-rate'\n | 'respiratory-rate'\n | 'oxygen-saturation'\n | 'blood-glucose'\n | 'body-temperature'\n | 'basal-body-temperature'\n | 'body-fat'\n | 'flights-climbed'\n | 'exercise-time'\n | 'distance-cycling'\n | 'sleep'\n | 'mindfulness'\n | 'hrv'\n | '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 systolic?: number;\n diastolic?: number;\n unit?: string;\n}\n\nexport interface QueryLatestSampleResponse {\n value?: number;\n systolic?: number;\n diastolic?: number;\n timestamp: number;\n endTimestamp?: number;\n unit: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface CharacteristicsResponse {\n biologicalSex?: HealthBiologicalSex | null;\n bloodType?: HealthBloodType | null;\n dateOfBirth?: string | null;\n fitzpatrickSkinType?: HealthFitzpatrickSkinType | null;\n wheelchairUse?: HealthWheelchairUse | null;\n /**\n * Indicates whether the platform exposes these characteristics via the plugin (true on iOS, false on Android).\n */\n platformSupported?: boolean;\n /**\n * Optional platform-specific message; on Android we return a user-facing note explaining that values remain empty unless synced from iOS.\n */\n platformMessage?: string;\n}\n\nexport type HealthBiologicalSex = 'female' | 'male' | 'other' | 'not_set' | 'unknown';\n\nexport type HealthBloodType =\n | 'a-positive'\n | 'a-negative'\n | 'b-positive'\n | 'b-negative'\n | 'ab-positive'\n | 'ab-negative'\n | 'o-positive'\n | 'o-negative'\n | 'not_set'\n | 'unknown';\n\nexport type HealthFitzpatrickSkinType = 'type1' | 'type2' | 'type3' | 'type4' | 'type5' | 'type6' | 'not_set' | 'unknown';\n\nexport type HealthWheelchairUse = 'wheelchair_user' | 'not_wheelchair_user' | 'not_set' | 'unknown';\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 * iOS only: Reads user characteristics such as biological sex, blood type, date of birth, Fitzpatrick skin type, and wheelchair use.\n * Values are null when unavailable or permission was not granted. Android does not expose these characteristics; it returns `platformSupported: false` and a `platformMessage` for UI hints without emitting null values.\n */\n getCharacteristics(): Promise<CharacteristicsResponse>;\n\n /**\n * Query aggregated data\n * - Blood-pressure aggregates return the systolic average in `value` plus `systolic`, `diastolic`, and `unit`.\n * - `total-calories` is derived as active + basal energy on both iOS and Android for latest samples, aggregated queries, and workouts. We fall back to the platform's total‑calories metric (or active calories) when basal data isn't available or permission is missing. Request both `READ_ACTIVE_CALORIES` and `READ_BASAL_CALORIES` for full totals.\n * - Weight/height aggregation returns the latest sample per day (no averaging).\n * - Android aggregation currently supports daily buckets; unsupported buckets will be rejected.\n * - Android `distance-cycling` aggregates distance recorded during biking exercise sessions (requires distance + workouts permissions).\n * - Daily `bucket: \"day\"` queries use calendar-day boundaries in the device time zone (start-of-day through the next start-of-day) instead of a trailing 24-hour window. For “today,” send `startDate` at today’s start-of-day and `endDate` at now or tomorrow’s start-of-day.\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 /**\n * Query latest sample for a specific data type\n * - Latest sleep sample returns the most recent complete sleep session (asleep states only) from the last ~36 hours; if a longer overnight session exists, shorter naps are ignored.\n * - `sleep-rem` returns REM duration (minutes) for the latest sleep session; requires iOS 16+ sleep stages and Health Connect REM data on Android.\n * @param request\n */\n queryLatestSample(request: { dataType: LatestDataType }): Promise<QueryLatestSampleResponse>;\n\n /**\n * Query latest weight sample\n */\n queryWeight(): Promise<QueryLatestSampleResponse>;\n\n /**\n * Query latest height sample\n */\n queryHeight(): Promise<QueryLatestSampleResponse>;\n\n /**\n * Query latest heart rate sample\n */\n queryHeartRate(): Promise<QueryLatestSampleResponse>;\n\n /**\n * Query latest steps sample\n */\n querySteps(): Promise<QueryLatestSampleResponse>;\n\n /**\n * Create a workout session with optional totals and route/heart-rate samples.\n * - iOS stores an `HKWorkout` (activityType mapped from `activityType`) with total energy/distance and optional metadata/route/heart-rate samples.\n * - Android stores an `ExerciseSessionRecord` plus `ActiveCaloriesBurnedRecord`, `DistanceRecord`, and `HeartRateRecord` when provided. Routes are attached via `ExerciseRoute`.\n * - Requires matching WRITE_* permissions for the values you include (e.g., WRITE_WORKOUTS + WRITE_ACTIVE_CALORIES + WRITE_DISTANCE + WRITE_HEART_RATE + WRITE_ROUTE).\n */\n saveWorkout(request: SaveWorkoutRequest): Promise<SaveWorkoutResponse>;\n\n /**\n * Save user-provided body metrics to the health platform.\n */\n saveMetrics(request: SaveMetricsRequest): Promise<SaveMetricsResponse>;\n}\n\nexport declare type HealthPermission = 'READ_STEPS' | 'READ_WORKOUTS' | 'WRITE_WORKOUTS' | 'READ_ACTIVE_CALORIES' | 'WRITE_ACTIVE_CALORIES' | 'READ_TOTAL_CALORIES' | 'WRITE_TOTAL_CALORIES' | 'READ_DISTANCE' | 'WRITE_DISTANCE' | 'READ_WEIGHT' | 'WRITE_WEIGHT' | 'READ_HEIGHT' | 'WRITE_HEIGHT' | 'READ_HEART_RATE' | 'WRITE_HEART_RATE' | 'READ_RESTING_HEART_RATE' | 'WRITE_RESTING_HEART_RATE' | 'READ_ROUTE' | 'WRITE_ROUTE' | 'READ_MINDFULNESS' | 'READ_HRV' | 'READ_BLOOD_PRESSURE' | 'READ_BASAL_CALORIES' | 'READ_RESPIRATORY_RATE' | 'READ_OXYGEN_SATURATION' | 'READ_BLOOD_GLUCOSE' | 'READ_BODY_TEMPERATURE' | 'READ_BASAL_BODY_TEMPERATURE' | 'READ_BODY_FAT' | 'WRITE_BODY_FAT' | 'READ_FLOORS_CLIMBED' | 'READ_SLEEP' | 'READ_EXERCISE_TIME' | 'READ_BIOLOGICAL_SEX' | 'READ_BLOOD_TYPE' | 'READ_DATE_OF_BIRTH' | 'READ_FITZPATRICK_SKIN_TYPE' | 'READ_WHEELCHAIR_USE';\n\nexport type LatestDataType =\n | 'steps'\n | 'active-calories'\n | 'total-calories'\n | 'basal-calories'\n | 'distance'\n | 'weight'\n | 'height'\n | 'heart-rate'\n | 'resting-heart-rate'\n | 'respiratory-rate'\n | 'oxygen-saturation'\n | 'blood-glucose'\n | 'body-temperature'\n | 'basal-body-temperature'\n | 'body-fat'\n | 'flights-climbed'\n | 'exercise-time'\n | 'distance-cycling'\n | 'mindfulness'\n | 'sleep'\n | 'sleep-rem'\n | 'hrv'\n | 'blood-pressure';\n\nexport interface PermissionsRequest {\n permissions: HealthPermission[];\n}\n\nexport interface PermissionResponse {\n permissions: Record<HealthPermission, 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 type WorkoutActivityType =\n | 'rock-climbing'\n | 'climbing'\n | 'hiking'\n | 'running'\n | 'walking'\n | 'cycling'\n | 'biking'\n | 'strength-training'\n | 'yoga'\n | 'other';\n\nexport interface SaveWorkoutRequest {\n activityType: WorkoutActivityType;\n startDate: string;\n endDate: string;\n calories?: number;\n distance?: number;\n metadata?: Record<string, any>;\n route?: RouteSample[];\n heartRateSamples?: HeartRateSample[];\n}\n\nexport interface SaveWorkoutResponse {\n success: boolean;\n id?: string;\n}\n\nexport interface SaveMetricsRequest {\n weightKg?: number;\n heightCm?: number;\n bodyFatPercent?: number;\n restingHeartRate?: number;\n}\n\nexport interface SaveMetricsResponse {\n success: boolean;\n inserted?: number;\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:\n | 'steps'\n | 'active-calories'\n | 'total-calories'\n | 'basal-calories'\n | 'distance'\n | 'weight'\n | 'height'\n | 'heart-rate'\n | 'resting-heart-rate'\n | 'respiratory-rate'\n | 'oxygen-saturation'\n | 'blood-glucose'\n | 'body-temperature'\n | 'basal-body-temperature'\n | 'body-fat'\n | 'flights-climbed'\n | 'exercise-time'\n | 'distance-cycling'\n | 'sleep'\n | 'mindfulness'\n | 'hrv'\n | '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 systolic?: number;\n diastolic?: number;\n unit?: string;\n}\n\nexport interface QueryLatestSampleResponse {\n value?: number;\n systolic?: number;\n diastolic?: number;\n timestamp: number;\n endTimestamp?: number;\n unit: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface CharacteristicsResponse {\n biologicalSex?: HealthBiologicalSex | null;\n bloodType?: HealthBloodType | null;\n dateOfBirth?: string | null;\n fitzpatrickSkinType?: HealthFitzpatrickSkinType | null;\n wheelchairUse?: HealthWheelchairUse | null;\n /**\n * Indicates whether the platform exposes these characteristics via the plugin (true on iOS, false on Android).\n */\n platformSupported?: boolean;\n /**\n * Optional platform-specific message; on Android we return a user-facing note explaining that values remain empty unless synced from iOS.\n */\n platformMessage?: string;\n}\n\nexport type HealthBiologicalSex = 'female' | 'male' | 'other' | 'not_set' | 'unknown';\n\nexport type HealthBloodType =\n | 'a-positive'\n | 'a-negative'\n | 'b-positive'\n | 'b-negative'\n | 'ab-positive'\n | 'ab-negative'\n | 'o-positive'\n | 'o-negative'\n | 'not_set'\n | 'unknown';\n\nexport type HealthFitzpatrickSkinType = 'type1' | 'type2' | 'type3' | 'type4' | 'type5' | 'type6' | 'not_set' | 'unknown';\n\nexport type HealthWheelchairUse = 'wheelchair_user' | 'not_wheelchair_user' | 'not_set' | 'unknown';\n"]}
|
|
@@ -21,13 +21,14 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
21
21
|
CAPPluginMethod(name: "queryWorkouts", returnType: CAPPluginReturnPromise),
|
|
22
22
|
CAPPluginMethod(name: "queryLatestSample", returnType: CAPPluginReturnPromise),
|
|
23
23
|
CAPPluginMethod(name: "getCharacteristics", returnType: CAPPluginReturnPromise),
|
|
24
|
+
CAPPluginMethod(name: "saveMetrics", returnType: CAPPluginReturnPromise),
|
|
24
25
|
CAPPluginMethod(name: "saveWorkout", returnType: CAPPluginReturnPromise)
|
|
25
26
|
]
|
|
26
27
|
|
|
27
28
|
let healthStore = HKHealthStore()
|
|
28
29
|
|
|
29
30
|
/// Serial queue to make route‑location mutations thread‑safe without locks
|
|
30
|
-
private let routeSyncQueue = DispatchQueue(label: "com.flomentumsolutions.
|
|
31
|
+
private let routeSyncQueue = DispatchQueue(label: "com.flomentumsolutions.capacitorhealthextended.routeSync")
|
|
31
32
|
|
|
32
33
|
@objc func isHealthAvailable(_ call: CAPPluginCall) {
|
|
33
34
|
let isAvailable = HKHealthStore.isHealthDataAvailable()
|
|
@@ -623,6 +624,90 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
623
624
|
}
|
|
624
625
|
}
|
|
625
626
|
|
|
627
|
+
@objc func saveMetrics(_ call: CAPPluginCall) {
|
|
628
|
+
guard HKHealthStore.isHealthDataAvailable() else {
|
|
629
|
+
call.reject("Health data is unavailable on this device.")
|
|
630
|
+
return
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
let weightKg = call.getDouble("weightKg")
|
|
634
|
+
let heightCm = call.getDouble("heightCm")
|
|
635
|
+
let bodyFatPercent = call.getDouble("bodyFatPercent")
|
|
636
|
+
let restingHeartRate = call.getDouble("restingHeartRate")
|
|
637
|
+
|
|
638
|
+
if weightKg == nil && heightCm == nil && bodyFatPercent == nil && restingHeartRate == nil {
|
|
639
|
+
call.reject("No metrics provided")
|
|
640
|
+
return
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
let shareTypes: Set<HKSampleType> = [
|
|
644
|
+
weightKg != nil ? HKObjectType.quantityType(forIdentifier: .bodyMass) : nil,
|
|
645
|
+
heightCm != nil ? HKObjectType.quantityType(forIdentifier: .height) : nil,
|
|
646
|
+
bodyFatPercent != nil ? HKObjectType.quantityType(forIdentifier: .bodyFatPercentage) : nil,
|
|
647
|
+
restingHeartRate != nil ? HKObjectType.quantityType(forIdentifier: .restingHeartRate) : nil
|
|
648
|
+
].compactMap { $0 as? HKSampleType }.reduce(into: Set<HKSampleType>()) { $0.insert($1) }
|
|
649
|
+
|
|
650
|
+
guard !shareTypes.isEmpty else {
|
|
651
|
+
call.reject("No valid metric types available on this device")
|
|
652
|
+
return
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
healthStore.requestAuthorization(toShare: shareTypes, read: nil) { [weak self] success, error in
|
|
656
|
+
guard let self = self else { return }
|
|
657
|
+
DispatchQueue.main.async {
|
|
658
|
+
if let error = error {
|
|
659
|
+
call.reject("Authorization failed: \(error.localizedDescription)")
|
|
660
|
+
return
|
|
661
|
+
}
|
|
662
|
+
guard success else {
|
|
663
|
+
call.reject("Authorization not granted for metrics")
|
|
664
|
+
return
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
let now = Date()
|
|
668
|
+
var samples: [HKSample] = []
|
|
669
|
+
|
|
670
|
+
if let weightKg = weightKg, let type = HKObjectType.quantityType(forIdentifier: .bodyMass) {
|
|
671
|
+
let quantity = HKQuantity(unit: HKUnit.gramUnit(with: .kilo), doubleValue: weightKg)
|
|
672
|
+
samples.append(HKQuantitySample(type: type, quantity: quantity, start: now, end: now, metadata: nil))
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if let heightCm = heightCm, let type = HKObjectType.quantityType(forIdentifier: .height) {
|
|
676
|
+
let quantity = HKQuantity(unit: HKUnit.meter(), doubleValue: heightCm / 100.0)
|
|
677
|
+
samples.append(HKQuantitySample(type: type, quantity: quantity, start: now, end: now, metadata: nil))
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if let bodyFatPercent = bodyFatPercent, let type = HKObjectType.quantityType(forIdentifier: .bodyFatPercentage) {
|
|
681
|
+
let quantity = HKQuantity(unit: HKUnit.percent(), doubleValue: bodyFatPercent / 100.0)
|
|
682
|
+
samples.append(HKQuantitySample(type: type, quantity: quantity, start: now, end: now, metadata: nil))
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if let restingHeartRate = restingHeartRate, let type = HKObjectType.quantityType(forIdentifier: .restingHeartRate) {
|
|
686
|
+
let unit = HKUnit.count().unitDivided(by: HKUnit.minute())
|
|
687
|
+
let quantity = HKQuantity(unit: unit, doubleValue: restingHeartRate)
|
|
688
|
+
samples.append(HKQuantitySample(type: type, quantity: quantity, start: now, end: now, metadata: nil))
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
guard !samples.isEmpty else {
|
|
692
|
+
call.reject("No metrics to save after validation")
|
|
693
|
+
return
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
self.healthStore.save(samples) { saveSuccess, saveError in
|
|
697
|
+
DispatchQueue.main.async {
|
|
698
|
+
if let saveError = saveError {
|
|
699
|
+
call.reject("Failed to save metrics: \(saveError.localizedDescription)")
|
|
700
|
+
} else if !saveSuccess {
|
|
701
|
+
call.reject("Failed to save metrics")
|
|
702
|
+
} else {
|
|
703
|
+
call.resolve(["success": true, "inserted": samples.count])
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
626
711
|
// Permission helpers
|
|
627
712
|
func permissionToHKObjectType(_ permission: String) -> [HKObjectType] {
|
|
628
713
|
switch permission {
|
|
@@ -660,6 +745,14 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
660
745
|
return [HKSeriesType.workoutRoute()].compactMap{$0}
|
|
661
746
|
case "WRITE_ROUTE":
|
|
662
747
|
return [HKSeriesType.workoutRoute()].compactMap{$0}
|
|
748
|
+
case "WRITE_WEIGHT":
|
|
749
|
+
return [HKObjectType.quantityType(forIdentifier: .bodyMass)].compactMap { $0 as? HKSampleType }
|
|
750
|
+
case "WRITE_HEIGHT":
|
|
751
|
+
return [HKObjectType.quantityType(forIdentifier: .height)].compactMap { $0 as? HKSampleType }
|
|
752
|
+
case "WRITE_BODY_FAT":
|
|
753
|
+
return [HKObjectType.quantityType(forIdentifier: .bodyFatPercentage)].compactMap { $0 as? HKSampleType }
|
|
754
|
+
case "WRITE_RESTING_HEART_RATE":
|
|
755
|
+
return [HKObjectType.quantityType(forIdentifier: .restingHeartRate)].compactMap { $0 as? HKSampleType }
|
|
663
756
|
case "READ_DISTANCE":
|
|
664
757
|
return [
|
|
665
758
|
HKObjectType.quantityType(forIdentifier: .distanceCycling),
|
|
@@ -1695,7 +1788,7 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1695
1788
|
return
|
|
1696
1789
|
}
|
|
1697
1790
|
let outerGroup = DispatchGroup()
|
|
1698
|
-
let resultsQueue = DispatchQueue(label: "com.flomentumsolutions.
|
|
1791
|
+
let resultsQueue = DispatchQueue(label: "com.flomentumsolutions.capacitorhealthextended.workoutResults")
|
|
1699
1792
|
var workoutResults: [[String: Any]] = []
|
|
1700
1793
|
var errors: [String: String] = [:]
|
|
1701
1794
|
|
|
@@ -1713,8 +1806,8 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1713
1806
|
"distance": workout.totalDistance?.doubleValue(for: .meter()) ?? 0
|
|
1714
1807
|
]
|
|
1715
1808
|
let innerGroup = DispatchGroup()
|
|
1716
|
-
let heartRateQueue = DispatchQueue(label: "com.flomentumsolutions.
|
|
1717
|
-
let routeQueue = DispatchQueue(label: "com.flomentumsolutions.
|
|
1809
|
+
let heartRateQueue = DispatchQueue(label: "com.flomentumsolutions.capacitorhealthextended.heartRates")
|
|
1810
|
+
let routeQueue = DispatchQueue(label: "com.flomentumsolutions.capacitorhealthextended.routes")
|
|
1718
1811
|
var localHeartRates: [[String: Any]] = []
|
|
1719
1812
|
var localRoutes: [[String: Any]] = []
|
|
1720
1813
|
|
|
@@ -1813,7 +1906,7 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1813
1906
|
guard let self = self else { return }
|
|
1814
1907
|
if let routes = samples as? [HKWorkoutRoute], error == nil {
|
|
1815
1908
|
let routeDispatchGroup = DispatchGroup()
|
|
1816
|
-
let allLocationsQueue = DispatchQueue(label: "com.flomentumsolutions.
|
|
1909
|
+
let allLocationsQueue = DispatchQueue(label: "com.flomentumsolutions.capacitorhealthextended.allLocations")
|
|
1817
1910
|
var allLocations: [[String: Any]] = []
|
|
1818
1911
|
|
|
1819
1912
|
for route in routes {
|
package/package.json
CHANGED