@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 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&lt;<a href="#savemetricsresponse">SaveMetricsResponse</a>&gt;</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
@@ -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.health.capacitor"
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.health.capacitor">
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.health.capacitor
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(calories),
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(calories),
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(distance),
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(altitudeValue)
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.healthplugin.routeSync")
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.healthplugin.workoutResults")
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.healthplugin.heartRates")
1717
- let routeQueue = DispatchQueue(label: "com.flomentumsolutions.healthplugin.routes")
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.healthplugin.allLocations")
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flomentumsolutions/capacitor-health-extended",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "description": "Capacitor plugin for Apple HealthKit and Google Health Connect Platform",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",