@flomentumsolutions/capacitor-health-extended 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/FlomentumsolutionsCapacitorHealthExtended.podspec +7 -4
- package/README.md +19 -14
- package/android/src/main/java/com/flomentum/health/capacitor/HealthPlugin.kt +48 -4
- package/dist/esm/definitions.d.ts +2 -2
- package/dist/esm/definitions.js.map +1 -1
- package/dist/plugin.js +1 -1
- package/ios/Sources/HealthPluginPlugin/HealthPlugin.swift +142 -31
- package/package.json +2 -2
- package/Package.swift +0 -28
|
@@ -10,8 +10,11 @@ Pod::Spec.new do |s|
|
|
|
10
10
|
s.homepage = package['repository']['url']
|
|
11
11
|
s.author = package['author']
|
|
12
12
|
s.source = { :git => package['repository']['url'], :tag => s.version.to_s }
|
|
13
|
-
|
|
13
|
+
# Only include Swift/Obj-C source files that belong to the plugin
|
|
14
|
+
s.source_files = 'ios/Sources/HealthPluginPlugin/**/*.{swift,h,m}'
|
|
14
15
|
s.ios.deployment_target = '13.0'
|
|
15
|
-
s.dependency 'Capacitor'
|
|
16
|
-
s.
|
|
17
|
-
|
|
16
|
+
s.dependency 'Capacitor', '~> 6.2'
|
|
17
|
+
s.dependency 'CapacitorCordova', '~> 6.2'
|
|
18
|
+
# Match the Swift shipped with Xcode 16 (use 5.9 for Xcode 15.x)
|
|
19
|
+
s.swift_version = '6.0'
|
|
20
|
+
end
|
package/README.md
CHANGED
|
@@ -35,14 +35,17 @@ npx cap sync
|
|
|
35
35
|
</queries>
|
|
36
36
|
|
|
37
37
|
<!-- Declare permissions you’ll request -->
|
|
38
|
-
<uses-permission android:name="android.permission.
|
|
39
|
-
<uses-permission android:name="android.permission.health.
|
|
40
|
-
<uses-permission android:name="android.permission.health.
|
|
38
|
+
<uses-permission android:name="android.permission.health.READ_STEPS" />
|
|
39
|
+
<uses-permission android:name="android.permission.health.READ_ACTIVE_CALORIES_BURNED" />
|
|
40
|
+
<uses-permission android:name="android.permission.health.READ_TOTAL_CALORIES_BURNED" />
|
|
41
|
+
<uses-permission android:name="android.permission.health.READ_DISTANCE" />
|
|
42
|
+
<uses-permission android:name="android.permission.health.READ_EXERCISE" />
|
|
43
|
+
<uses-permission android:name="android.permission.health.READ_EXERCISE_ROUTE" />
|
|
44
|
+
<uses-permission android:name="android.permission.health.READ_HEART_RATE" />
|
|
41
45
|
<uses-permission android:name="android.permission.health.READ_WEIGHT" />
|
|
42
|
-
<uses-permission android:name="android.permission.health.
|
|
43
|
-
<uses-permission android:name="android.permission.health.
|
|
44
|
-
<uses-permission android:name="android.permission.health.
|
|
45
|
-
<uses-permission android:name="android.permission.health.READ_HEART_RATE"/>
|
|
46
|
+
<uses-permission android:name="android.permission.health.READ_HEIGHT" />
|
|
47
|
+
<uses-permission android:name="android.permission.health.READ_HEART_RATE_VARIABILITY" />
|
|
48
|
+
<uses-permission android:name="android.permission.health.READ_BLOOD_PRESSURE" />
|
|
46
49
|
```
|
|
47
50
|
|
|
48
51
|
|
|
@@ -277,12 +280,12 @@ Query workouts
|
|
|
277
280
|
|
|
278
281
|
#### QueryAggregatedRequest
|
|
279
282
|
|
|
280
|
-
| Prop | Type
|
|
281
|
-
| --------------- |
|
|
282
|
-
| **`startDate`** | <code>string</code>
|
|
283
|
-
| **`endDate`** | <code>string</code>
|
|
284
|
-
| **`dataType`** | <code>'steps' \| 'calories'</code> |
|
|
285
|
-
| **`bucket`** | <code>string</code>
|
|
283
|
+
| Prop | Type |
|
|
284
|
+
| --------------- | --------------------------------------------------------------------------------------- |
|
|
285
|
+
| **`startDate`** | <code>string</code> |
|
|
286
|
+
| **`endDate`** | <code>string</code> |
|
|
287
|
+
| **`dataType`** | <code>'steps' \| 'active-calories' \| 'mindfulness' \| 'hrv' \| 'blood-pressure'</code> |
|
|
288
|
+
| **`bucket`** | <code>string</code> |
|
|
286
289
|
|
|
287
290
|
|
|
288
291
|
#### QueryWorkoutResponse
|
|
@@ -303,6 +306,7 @@ Query workouts
|
|
|
303
306
|
| **`id`** | <code>string</code> |
|
|
304
307
|
| **`duration`** | <code>number</code> |
|
|
305
308
|
| **`distance`** | <code>number</code> |
|
|
309
|
+
| **`steps`** | <code>number</code> |
|
|
306
310
|
| **`calories`** | <code>number</code> |
|
|
307
311
|
| **`sourceBundleId`** | <code>string</code> |
|
|
308
312
|
| **`route`** | <code>RouteSample[]</code> |
|
|
@@ -335,6 +339,7 @@ Query workouts
|
|
|
335
339
|
| **`endDate`** | <code>string</code> |
|
|
336
340
|
| **`includeHeartRate`** | <code>boolean</code> |
|
|
337
341
|
| **`includeRoute`** | <code>boolean</code> |
|
|
342
|
+
| **`includeSteps`** | <code>boolean</code> |
|
|
338
343
|
|
|
339
344
|
|
|
340
345
|
### Type Aliases
|
|
@@ -342,6 +347,6 @@ Query workouts
|
|
|
342
347
|
|
|
343
348
|
#### HealthPermission
|
|
344
349
|
|
|
345
|
-
<code>'READ_STEPS' | 'READ_WORKOUTS' | '
|
|
350
|
+
<code>'READ_STEPS' | 'READ_WORKOUTS' | 'READ_ACTIVE_CALORIES' | 'READ_TOTAL_CALORIES' | 'READ_DISTANCE' | 'READ_WEIGHT' | 'READ_HEIGHT' | 'READ_HEART_RATE' | 'READ_ROUTE' | 'READ_MINDFULNESS' | 'READ_HRV' | 'READ_BLOOD_PRESSURE'</code>
|
|
346
351
|
|
|
347
352
|
</docgen-api>
|
|
@@ -32,7 +32,8 @@ import java.util.concurrent.atomic.AtomicReference
|
|
|
32
32
|
import androidx.core.net.toUri
|
|
33
33
|
|
|
34
34
|
enum class CapHealthPermission {
|
|
35
|
-
READ_STEPS, READ_WORKOUTS, READ_HEART_RATE, READ_ACTIVE_CALORIES, READ_TOTAL_CALORIES, READ_DISTANCE, READ_WEIGHT
|
|
35
|
+
READ_STEPS, READ_WORKOUTS, READ_HEART_RATE, READ_ACTIVE_CALORIES, READ_TOTAL_CALORIES, READ_DISTANCE, READ_WEIGHT
|
|
36
|
+
, READ_HRV, READ_BLOOD_PRESSURE;
|
|
36
37
|
|
|
37
38
|
companion object {
|
|
38
39
|
fun from(s: String): CapHealthPermission? {
|
|
@@ -50,11 +51,14 @@ enum class CapHealthPermission {
|
|
|
50
51
|
permissions = [
|
|
51
52
|
Permission(alias = "READ_STEPS", strings = ["android.permission.health.READ_STEPS"]),
|
|
52
53
|
Permission(alias = "READ_WEIGHT", strings = ["android.permission.health.READ_WEIGHT"]),
|
|
54
|
+
Permission(alias = "READ_HEIGHT", strings = ["android.permission.health.READ_HEIGHT"]),
|
|
53
55
|
Permission(alias = "READ_WORKOUTS", strings = ["android.permission.health.READ_EXERCISE"]),
|
|
54
56
|
Permission(alias = "READ_DISTANCE", strings = ["android.permission.health.READ_DISTANCE"]),
|
|
55
57
|
Permission(alias = "READ_ACTIVE_CALORIES", strings = ["android.permission.health.READ_ACTIVE_CALORIES_BURNED"]),
|
|
56
58
|
Permission(alias = "READ_TOTAL_CALORIES", strings = ["android.permission.health.READ_TOTAL_CALORIES_BURNED"]),
|
|
57
|
-
Permission(alias = "READ_HEART_RATE", strings = ["android.permission.health.READ_HEART_RATE"])
|
|
59
|
+
Permission(alias = "READ_HEART_RATE", strings = ["android.permission.health.READ_HEART_RATE"]),
|
|
60
|
+
Permission(alias = "READ_HRV", strings = ["android.permission.health.READ_HEART_RATE_VARIABILITY"]),
|
|
61
|
+
Permission(alias = "READ_BLOOD_PRESSURE", strings = ["android.permission.health.READ_BLOOD_PRESSURE"])
|
|
58
62
|
]
|
|
59
63
|
)
|
|
60
64
|
|
|
@@ -75,7 +79,9 @@ class HealthPlugin : Plugin() {
|
|
|
75
79
|
CapHealthPermission.READ_ACTIVE_CALORIES to HealthPermission.getReadPermission(ActiveCaloriesBurnedRecord::class),
|
|
76
80
|
CapHealthPermission.READ_TOTAL_CALORIES to HealthPermission.getReadPermission(TotalCaloriesBurnedRecord::class),
|
|
77
81
|
CapHealthPermission.READ_DISTANCE to HealthPermission.getReadPermission(DistanceRecord::class),
|
|
78
|
-
CapHealthPermission.READ_WORKOUTS to HealthPermission.getReadPermission(ExerciseSessionRecord::class)
|
|
82
|
+
CapHealthPermission.READ_WORKOUTS to HealthPermission.getReadPermission(ExerciseSessionRecord::class),
|
|
83
|
+
CapHealthPermission.READ_HRV to HealthPermission.getReadPermission(HeartRateVariabilitySdnnRecord::class),
|
|
84
|
+
CapHealthPermission.READ_BLOOD_PRESSURE to HealthPermission.getReadPermission(BloodPressureRecord::class)
|
|
79
85
|
)
|
|
80
86
|
|
|
81
87
|
override fun load() {
|
|
@@ -258,6 +264,7 @@ class HealthPlugin : Plugin() {
|
|
|
258
264
|
TotalCaloriesBurnedRecord.ENERGY_TOTAL
|
|
259
265
|
) { it?.inKilocalories }
|
|
260
266
|
"distance" -> metricAndMapper("distance", CapHealthPermission.READ_DISTANCE, DistanceRecord.DISTANCE_TOTAL) { it?.inMeters }
|
|
267
|
+
"hrv" -> metricAndMapper("hrv", CapHealthPermission.READ_HRV, HeartRateVariabilitySdnnRecord.SDNN_AVG) { it }
|
|
261
268
|
else -> throw RuntimeException("Unsupported dataType: $dataType")
|
|
262
269
|
}
|
|
263
270
|
}
|
|
@@ -277,6 +284,8 @@ class HealthPlugin : Plugin() {
|
|
|
277
284
|
"heart-rate" -> readLatestHeartRate()
|
|
278
285
|
"weight" -> readLatestWeight()
|
|
279
286
|
"steps" -> readLatestSteps()
|
|
287
|
+
"hrv" -> readLatestHrv()
|
|
288
|
+
"blood-pressure" -> readLatestBloodPressure()
|
|
280
289
|
else -> {
|
|
281
290
|
call.reject("Unsupported data type: $dataType")
|
|
282
291
|
return@launch
|
|
@@ -284,7 +293,7 @@ class HealthPlugin : Plugin() {
|
|
|
284
293
|
}
|
|
285
294
|
call.resolve(result)
|
|
286
295
|
} catch (e: Exception) {
|
|
287
|
-
Log.e(tag, "queryLatestSample: Error fetching latest
|
|
296
|
+
Log.e(tag, "queryLatestSample: Error fetching latest $dataType", e)
|
|
288
297
|
call.reject("Error fetching latest $dataType: ${e.message}")
|
|
289
298
|
}
|
|
290
299
|
}
|
|
@@ -344,6 +353,41 @@ class HealthPlugin : Plugin() {
|
|
|
344
353
|
}
|
|
345
354
|
}
|
|
346
355
|
|
|
356
|
+
private suspend fun readLatestHrv(): JSObject {
|
|
357
|
+
if (!hasPermission(CapHealthPermission.READ_HRV)) {
|
|
358
|
+
throw Exception("Permission for HRV not granted")
|
|
359
|
+
}
|
|
360
|
+
val request = ReadRecordsRequest(
|
|
361
|
+
recordType = HeartRateVariabilitySdnnRecord::class,
|
|
362
|
+
timeRangeFilter = TimeRangeFilter.after(Instant.EPOCH),
|
|
363
|
+
pageSize = 1
|
|
364
|
+
)
|
|
365
|
+
val result = healthConnectClient.readRecords(request)
|
|
366
|
+
val record = result.records.firstOrNull() ?: throw Exception("No HRV data found")
|
|
367
|
+
return JSObject().apply {
|
|
368
|
+
put("timestamp", record.time.toString())
|
|
369
|
+
put("value", record.sdnnMillis)
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private suspend fun readLatestBloodPressure(): JSObject {
|
|
374
|
+
if (!hasPermission(CapHealthPermission.READ_BLOOD_PRESSURE)) {
|
|
375
|
+
throw Exception("Permission for blood pressure not granted")
|
|
376
|
+
}
|
|
377
|
+
val request = ReadRecordsRequest(
|
|
378
|
+
recordType = BloodPressureRecord::class,
|
|
379
|
+
timeRangeFilter = TimeRangeFilter.after(Instant.EPOCH),
|
|
380
|
+
pageSize = 1
|
|
381
|
+
)
|
|
382
|
+
val result = healthConnectClient.readRecords(request)
|
|
383
|
+
val record = result.records.firstOrNull() ?: throw Exception("No blood pressure data found")
|
|
384
|
+
return JSObject().apply {
|
|
385
|
+
put("timestamp", record.time.toString())
|
|
386
|
+
put("systolic", record.systolic.inMillimetersOfMercury)
|
|
387
|
+
put("diastolic", record.diastolic.inMillimetersOfMercury)
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
347
391
|
@PluginMethod
|
|
348
392
|
fun queryAggregated(call: PluginCall) {
|
|
349
393
|
if (!ensureClientInitialized(call)) return
|
|
@@ -51,7 +51,7 @@ export interface HealthPlugin {
|
|
|
51
51
|
*/
|
|
52
52
|
queryWorkouts(request: QueryWorkoutRequest): Promise<QueryWorkoutResponse>;
|
|
53
53
|
}
|
|
54
|
-
export declare type HealthPermission = 'READ_STEPS' | 'READ_WORKOUTS' | 'READ_ACTIVE_CALORIES' | 'READ_TOTAL_CALORIES' | 'READ_DISTANCE' | 'READ_HEART_RATE' | 'READ_ROUTE' | 'READ_MINDFULNESS';
|
|
54
|
+
export declare type HealthPermission = 'READ_STEPS' | 'READ_WORKOUTS' | 'READ_ACTIVE_CALORIES' | 'READ_TOTAL_CALORIES' | 'READ_DISTANCE' | 'READ_WEIGHT' | 'READ_HEIGHT' | 'READ_HEART_RATE' | 'READ_ROUTE' | 'READ_MINDFULNESS' | 'READ_HRV' | 'READ_BLOOD_PRESSURE';
|
|
55
55
|
export interface PermissionsRequest {
|
|
56
56
|
permissions: HealthPermission[];
|
|
57
57
|
}
|
|
@@ -97,7 +97,7 @@ export interface Workout {
|
|
|
97
97
|
export interface QueryAggregatedRequest {
|
|
98
98
|
startDate: string;
|
|
99
99
|
endDate: string;
|
|
100
|
-
dataType: 'steps' | 'active-calories' | 'mindfulness';
|
|
100
|
+
dataType: 'steps' | 'active-calories' | 'mindfulness' | 'hrv' | 'blood-pressure';
|
|
101
101
|
bucket: string;
|
|
102
102
|
}
|
|
103
103
|
export interface QueryAggregatedResponse {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["export interface HealthPlugin {\n /**\n * Checks if health API is available.\n * Android: If false is returned, the Google Health Connect app is probably not installed.\n * See showHealthConnectInPlayStore()\n *\n */\n isHealthAvailable(): Promise<{ available: boolean }>;\n\n /**\n * Android only: Returns for each given permission, if it was granted by the underlying health API\n * @param permissions permissions to query\n */\n checkHealthPermissions(permissions: PermissionsRequest): Promise<PermissionResponse>;\n\n /**\n * Requests the permissions from the user.\n *\n * Android: Apps can ask only a few times for permissions, after that the user has to grant them manually in\n * the Health Connect app. See openHealthConnectSettings()\n *\n * iOS: If the permissions are already granted or denied, this method will just return without asking the user. In iOS\n * we can't really detect if a user granted or denied a permission. The return value reflects the assumption that all\n * permissions were granted.\n *\n * @param permissions permissions to request\n */\n requestHealthPermissions(permissions: PermissionsRequest): Promise<PermissionResponse>;\n\n /**\n * Opens the apps settings, which is kind of wrong, because health permissions are configured under:\n * Settings > Apps > (Apple) Health > Access and Devices > [app-name]\n * But we can't go there directly.\n */\n openAppleHealthSettings(): Promise<void>;\n\n /**\n * Opens the Google Health Connect app\n */\n openHealthConnectSettings(): Promise<void>;\n\n /**\n * Opens the Google Health Connect app in PlayStore\n */\n showHealthConnectInPlayStore(): Promise<void>;\n\n /**\n * Query aggregated data\n * @param request\n */\n queryAggregated(request: QueryAggregatedRequest): Promise<QueryAggregatedResponse>;\n\n /**\n * Query workouts\n * @param request\n */\n queryWorkouts(request: QueryWorkoutRequest): Promise<QueryWorkoutResponse>;\n}\n\nexport declare type HealthPermission =\n | 'READ_STEPS'\n | 'READ_WORKOUTS'\n | 'READ_ACTIVE_CALORIES'\n | 'READ_TOTAL_CALORIES'\n | 'READ_DISTANCE'\n | 'READ_HEART_RATE'\n | 'READ_ROUTE'\n | 'READ_MINDFULNESS';\n\nexport interface PermissionsRequest {\n permissions: HealthPermission[];\n}\n\nexport interface PermissionResponse {\n permissions: { [key: string]: boolean }[];\n}\n\nexport interface QueryWorkoutRequest {\n startDate: string;\n endDate: string;\n includeHeartRate: boolean;\n includeRoute: boolean;\n includeSteps: boolean;\n}\n\nexport interface HeartRateSample {\n timestamp: string;\n bpm: number;\n}\n\nexport interface RouteSample {\n timestamp: string;\n lat: number;\n lng: number;\n alt?: number;\n}\n\nexport interface QueryWorkoutResponse {\n workouts: Workout[];\n}\n\nexport interface Workout {\n startDate: string;\n endDate: string;\n workoutType: string;\n sourceName: string;\n id?: string;\n duration: number;\n distance?: number;\n steps?: number;\n calories: number;\n sourceBundleId: string;\n route?: RouteSample[];\n heartRate?: HeartRateSample[];\n}\n\nexport interface QueryAggregatedRequest {\n startDate: string;\n endDate: string;\n dataType: 'steps' | 'active-calories' | 'mindfulness';\n bucket: string;\n}\n\nexport interface QueryAggregatedResponse {\n aggregatedData: AggregatedSample[];\n}\n\nexport interface AggregatedSample {\n startDate: string;\n endDate: string;\n value: number;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["export interface HealthPlugin {\n /**\n * Checks if health API is available.\n * Android: If false is returned, the Google Health Connect app is probably not installed.\n * See showHealthConnectInPlayStore()\n *\n */\n isHealthAvailable(): Promise<{ available: boolean }>;\n\n /**\n * Android only: Returns for each given permission, if it was granted by the underlying health API\n * @param permissions permissions to query\n */\n checkHealthPermissions(permissions: PermissionsRequest): Promise<PermissionResponse>;\n\n /**\n * Requests the permissions from the user.\n *\n * Android: Apps can ask only a few times for permissions, after that the user has to grant them manually in\n * the Health Connect app. See openHealthConnectSettings()\n *\n * iOS: If the permissions are already granted or denied, this method will just return without asking the user. In iOS\n * we can't really detect if a user granted or denied a permission. The return value reflects the assumption that all\n * permissions were granted.\n *\n * @param permissions permissions to request\n */\n requestHealthPermissions(permissions: PermissionsRequest): Promise<PermissionResponse>;\n\n /**\n * Opens the apps settings, which is kind of wrong, because health permissions are configured under:\n * Settings > Apps > (Apple) Health > Access and Devices > [app-name]\n * But we can't go there directly.\n */\n openAppleHealthSettings(): Promise<void>;\n\n /**\n * Opens the Google Health Connect app\n */\n openHealthConnectSettings(): Promise<void>;\n\n /**\n * Opens the Google Health Connect app in PlayStore\n */\n showHealthConnectInPlayStore(): Promise<void>;\n\n /**\n * Query aggregated data\n * @param request\n */\n queryAggregated(request: QueryAggregatedRequest): Promise<QueryAggregatedResponse>;\n\n /**\n * Query workouts\n * @param request\n */\n queryWorkouts(request: QueryWorkoutRequest): Promise<QueryWorkoutResponse>;\n}\n\nexport declare type HealthPermission =\n | 'READ_STEPS'\n | 'READ_WORKOUTS'\n | 'READ_ACTIVE_CALORIES'\n | 'READ_TOTAL_CALORIES'\n | 'READ_DISTANCE'\n | 'READ_WEIGHT'\n | 'READ_HEIGHT'\n | 'READ_HEART_RATE'\n | 'READ_ROUTE'\n | 'READ_MINDFULNESS'\n | 'READ_HRV'\n | 'READ_BLOOD_PRESSURE';\n\nexport interface PermissionsRequest {\n permissions: HealthPermission[];\n}\n\nexport interface PermissionResponse {\n permissions: { [key: string]: boolean }[];\n}\n\nexport interface QueryWorkoutRequest {\n startDate: string;\n endDate: string;\n includeHeartRate: boolean;\n includeRoute: boolean;\n includeSteps: boolean;\n}\n\nexport interface HeartRateSample {\n timestamp: string;\n bpm: number;\n}\n\nexport interface RouteSample {\n timestamp: string;\n lat: number;\n lng: number;\n alt?: number;\n}\n\nexport interface QueryWorkoutResponse {\n workouts: Workout[];\n}\n\nexport interface Workout {\n startDate: string;\n endDate: string;\n workoutType: string;\n sourceName: string;\n id?: string;\n duration: number;\n distance?: number;\n steps?: number;\n calories: number;\n sourceBundleId: string;\n route?: RouteSample[];\n heartRate?: HeartRateSample[];\n}\n\nexport interface QueryAggregatedRequest {\n startDate: string;\n endDate: string;\n dataType: 'steps' | 'active-calories' | 'mindfulness' | 'hrv' | 'blood-pressure';\n bucket: string;\n}\n\nexport interface QueryAggregatedResponse {\n aggregatedData: AggregatedSample[];\n}\n\nexport interface AggregatedSample {\n startDate: string;\n endDate: string;\n value: number;\n}\n"]}
|
package/dist/plugin.js
CHANGED
|
@@ -86,6 +86,53 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
86
86
|
call.reject("Missing data type")
|
|
87
87
|
return
|
|
88
88
|
}
|
|
89
|
+
// ---- Special handling for blood‑pressure correlation ----
|
|
90
|
+
if dataTypeString == "blood-pressure" {
|
|
91
|
+
guard let bpType = HKObjectType.correlationType(forIdentifier: .bloodPressure) else {
|
|
92
|
+
call.reject("Blood pressure type not available")
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false)
|
|
97
|
+
let predicate = HKQuery.predicateForSamples(withStart: Date.distantPast, end: Date(), options: .strictEndDate)
|
|
98
|
+
|
|
99
|
+
let query = HKSampleQuery(sampleType: bpType, predicate: predicate, limit: 1, sortDescriptors: [sortDescriptor]) { _, samples, error in
|
|
100
|
+
|
|
101
|
+
guard let bpCorrelation = samples?.first as? HKCorrelation else {
|
|
102
|
+
if let error = error {
|
|
103
|
+
call.reject("Error fetching latest blood pressure sample", "NO_SAMPLE", error)
|
|
104
|
+
} else {
|
|
105
|
+
call.reject("No blood pressure sample found", "NO_SAMPLE")
|
|
106
|
+
}
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let unit = HKUnit.millimeterOfMercury()
|
|
111
|
+
|
|
112
|
+
let systolicSamples = bpCorrelation.objects(for: HKObjectType.quantityType(forIdentifier: .bloodPressureSystolic)!)
|
|
113
|
+
let diastolicSamples = bpCorrelation.objects(for: HKObjectType.quantityType(forIdentifier: .bloodPressureDiastolic)!)
|
|
114
|
+
|
|
115
|
+
guard let systolicSample = systolicSamples.first as? HKQuantitySample,
|
|
116
|
+
let diastolicSample = diastolicSamples.first as? HKQuantitySample else {
|
|
117
|
+
call.reject("Incomplete blood pressure data", "NO_SAMPLE")
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let systolicValue = systolicSample.quantity.doubleValue(for: unit)
|
|
122
|
+
let diastolicValue = diastolicSample.quantity.doubleValue(for: unit)
|
|
123
|
+
let timestamp = bpCorrelation.startDate.timeIntervalSince1970 * 1000
|
|
124
|
+
|
|
125
|
+
call.resolve([
|
|
126
|
+
"systolic": systolicValue,
|
|
127
|
+
"diastolic": diastolicValue,
|
|
128
|
+
"timestamp": timestamp,
|
|
129
|
+
"unit": unit.unitString
|
|
130
|
+
])
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
healthStore.execute(query)
|
|
134
|
+
return
|
|
135
|
+
}
|
|
89
136
|
guard aggregateTypeToHKQuantityType(dataTypeString) != nil else {
|
|
90
137
|
call.reject("Invalid data type")
|
|
91
138
|
return
|
|
@@ -99,6 +146,18 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
99
146
|
return HKObjectType.quantityType(forIdentifier: .bodyMass)
|
|
100
147
|
case "steps":
|
|
101
148
|
return HKObjectType.quantityType(forIdentifier: .stepCount)
|
|
149
|
+
case "hrv":
|
|
150
|
+
return HKObjectType.quantityType(forIdentifier: .heartRateVariabilitySDNN)
|
|
151
|
+
case "height":
|
|
152
|
+
return HKObjectType.quantityType(forIdentifier: .height)
|
|
153
|
+
case "distance":
|
|
154
|
+
return HKObjectType.quantityType(forIdentifier: .distanceWalkingRunning)
|
|
155
|
+
case "active-calories":
|
|
156
|
+
return HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)
|
|
157
|
+
case "total-calories":
|
|
158
|
+
return HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)
|
|
159
|
+
case "blood-pressure":
|
|
160
|
+
return nil // handled above
|
|
102
161
|
default:
|
|
103
162
|
return nil
|
|
104
163
|
}
|
|
@@ -130,8 +189,15 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
130
189
|
unit = HKUnit.count().unitDivided(by: HKUnit.minute())
|
|
131
190
|
} else if dataTypeString == "weight" {
|
|
132
191
|
unit = .gramUnit(with: .kilo)
|
|
192
|
+
} else if dataTypeString == "hrv" {
|
|
193
|
+
unit = HKUnit.secondUnit(with: .milli)
|
|
194
|
+
} else if dataTypeString == "distance" {
|
|
195
|
+
unit = HKUnit.meter()
|
|
196
|
+
} else if dataTypeString == "active-calories" || dataTypeString == "total-calories" {
|
|
197
|
+
unit = HKUnit.kilocalorie()
|
|
198
|
+
} else if dataTypeString == "height" {
|
|
199
|
+
unit = HKUnit.meter()
|
|
133
200
|
}
|
|
134
|
-
|
|
135
201
|
let value = quantitySample.quantity.doubleValue(for: unit)
|
|
136
202
|
let timestamp = quantitySample.startDate.timeIntervalSince1970 * 1000
|
|
137
203
|
|
|
@@ -163,6 +229,13 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
163
229
|
return [HKObjectType.quantityType(forIdentifier: .stepCount)].compactMap{$0}
|
|
164
230
|
case "READ_WEIGHT":
|
|
165
231
|
return [HKObjectType.quantityType(forIdentifier: .bodyMass)].compactMap{$0}
|
|
232
|
+
case "READ_HEIGHT":
|
|
233
|
+
return [HKObjectType.quantityType(forIdentifier: .height)].compactMap { $0 }
|
|
234
|
+
case "READ_TOTAL_CALORIES":
|
|
235
|
+
return [
|
|
236
|
+
HKObjectType.quantityType(forIdentifier: .activeEnergyBurned),
|
|
237
|
+
HKObjectType.quantityType(forIdentifier: .basalEnergyBurned) // iOS 16+
|
|
238
|
+
].compactMap { $0 }
|
|
166
239
|
case "READ_ACTIVE_CALORIES":
|
|
167
240
|
return [HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)].compactMap{$0}
|
|
168
241
|
case "READ_WORKOUTS":
|
|
@@ -180,6 +253,13 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
180
253
|
].compactMap{$0}
|
|
181
254
|
case "READ_MINDFULNESS":
|
|
182
255
|
return [HKObjectType.categoryType(forIdentifier: .mindfulSession)!].compactMap{$0}
|
|
256
|
+
case "READ_HRV":
|
|
257
|
+
return [HKObjectType.quantityType(forIdentifier: .heartRateVariabilitySDNN)].compactMap { $0 }
|
|
258
|
+
case "READ_BLOOD_PRESSURE":
|
|
259
|
+
return [
|
|
260
|
+
HKObjectType.quantityType(forIdentifier: .bloodPressureSystolic),
|
|
261
|
+
HKObjectType.quantityType(forIdentifier: .bloodPressureDiastolic)
|
|
262
|
+
].compactMap { $0 }
|
|
183
263
|
default:
|
|
184
264
|
return []
|
|
185
265
|
}
|
|
@@ -195,6 +275,14 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
195
275
|
return HKObjectType.quantityType(forIdentifier: .heartRate)
|
|
196
276
|
case "weight":
|
|
197
277
|
return HKObjectType.quantityType(forIdentifier: .bodyMass)
|
|
278
|
+
case "hrv":
|
|
279
|
+
return HKObjectType.quantityType(forIdentifier: .heartRateVariabilitySDNN)
|
|
280
|
+
case "distance":
|
|
281
|
+
return HKObjectType.quantityType(forIdentifier: .distanceWalkingRunning) // pick one rep type
|
|
282
|
+
case "total-calories":
|
|
283
|
+
return HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)
|
|
284
|
+
case "height":
|
|
285
|
+
return HKObjectType.quantityType(forIdentifier: .height)
|
|
198
286
|
default:
|
|
199
287
|
return nil
|
|
200
288
|
}
|
|
@@ -234,11 +322,12 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
234
322
|
}
|
|
235
323
|
|
|
236
324
|
let options: HKStatisticsOptions = {
|
|
237
|
-
switch dataType.
|
|
238
|
-
case
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
325
|
+
switch dataType.aggregationStyle {
|
|
326
|
+
case .cumulative:
|
|
327
|
+
return .cumulativeSum
|
|
328
|
+
case .discrete:
|
|
329
|
+
return .discreteAverage // or .discreteMin / Max when needed
|
|
330
|
+
@unknown default:
|
|
242
331
|
return .cumulativeSum
|
|
243
332
|
}
|
|
244
333
|
}()
|
|
@@ -250,41 +339,63 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
250
339
|
anchorDate: startDate,
|
|
251
340
|
intervalComponents: interval
|
|
252
341
|
)
|
|
253
|
-
|
|
342
|
+
|
|
254
343
|
query.initialResultsHandler = { query, result, error in
|
|
255
344
|
if let error = error {
|
|
256
345
|
call.reject("Error fetching aggregated data: \(error.localizedDescription)")
|
|
257
346
|
return
|
|
258
347
|
}
|
|
259
|
-
|
|
348
|
+
|
|
260
349
|
var aggregatedSamples: [[String: Any]] = []
|
|
261
|
-
|
|
262
|
-
result?.enumerateStatistics(from: startDate, to: endDate) { statistics,
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
350
|
+
|
|
351
|
+
result?.enumerateStatistics(from: startDate, to: endDate) { statistics, _ in
|
|
352
|
+
// Choose sum or average based on the options we picked
|
|
353
|
+
let quantity: HKQuantity? = options.contains(.cumulativeSum)
|
|
354
|
+
? statistics.sumQuantity()
|
|
355
|
+
: statistics.averageQuantity()
|
|
356
|
+
|
|
357
|
+
guard let quantity = quantity else { return }
|
|
358
|
+
|
|
359
|
+
// Time‑bounds of this bucket
|
|
360
|
+
let bucketStart = statistics.startDate.timeIntervalSince1970 * 1000
|
|
361
|
+
let bucketEnd = statistics.endDate.timeIntervalSince1970 * 1000
|
|
362
|
+
|
|
363
|
+
// Map dataType → correct HKUnit
|
|
364
|
+
let unit: HKUnit = {
|
|
365
|
+
switch dataTypeString {
|
|
366
|
+
case "steps":
|
|
367
|
+
return .count()
|
|
368
|
+
case "active-calories", "total-calories":
|
|
369
|
+
return .kilocalorie()
|
|
370
|
+
case "distance":
|
|
371
|
+
return .meter()
|
|
372
|
+
case "weight":
|
|
373
|
+
return .gramUnit(with: .kilo)
|
|
374
|
+
case "height":
|
|
375
|
+
return .meter()
|
|
376
|
+
case "heart-rate":
|
|
377
|
+
return HKUnit.count().unitDivided(by: HKUnit.minute())
|
|
378
|
+
case "hrv":
|
|
379
|
+
return HKUnit.secondUnit(with: .milli)
|
|
380
|
+
case "mindfulness":
|
|
381
|
+
return HKUnit.second()
|
|
382
|
+
default:
|
|
383
|
+
return .count()
|
|
274
384
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
385
|
+
}()
|
|
386
|
+
|
|
387
|
+
let value = quantity.doubleValue(for: unit)
|
|
388
|
+
|
|
389
|
+
aggregatedSamples.append([
|
|
390
|
+
"startDate": bucketStart,
|
|
391
|
+
"endDate": bucketEnd,
|
|
392
|
+
"value": value
|
|
393
|
+
])
|
|
283
394
|
}
|
|
284
|
-
|
|
395
|
+
|
|
285
396
|
call.resolve(["aggregatedData": aggregatedSamples])
|
|
286
397
|
}
|
|
287
|
-
|
|
398
|
+
|
|
288
399
|
healthStore.execute(query)
|
|
289
400
|
}
|
|
290
401
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flomentumsolutions/capacitor-health-extended",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "Capacitor plugin for Apple HealthKit and Google Health Connect Platform",
|
|
5
5
|
"main": "dist/plugin.cjs.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"license": "MIT",
|
|
20
20
|
"repository": {
|
|
21
21
|
"type": "git",
|
|
22
|
-
"url": "
|
|
22
|
+
"url": "https://github.com/Flomentum-Solutions/capacitor-health-extended.git"
|
|
23
23
|
},
|
|
24
24
|
"bugs": {
|
|
25
25
|
"url": "https://github.com/Flomentum-Solutions/capacitor-health-extended/issues"
|
package/Package.swift
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
// swift-tools-version: 5.9
|
|
2
|
-
import PackageDescription
|
|
3
|
-
|
|
4
|
-
let package = Package(
|
|
5
|
-
name: "CapacitorHealth",
|
|
6
|
-
platforms: [.iOS(.v13)],
|
|
7
|
-
products: [
|
|
8
|
-
.library(
|
|
9
|
-
name: "CapacitorHealth",
|
|
10
|
-
targets: ["HealthPluginPlugin"])
|
|
11
|
-
],
|
|
12
|
-
dependencies: [
|
|
13
|
-
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", branch: "main")
|
|
14
|
-
],
|
|
15
|
-
targets: [
|
|
16
|
-
.target(
|
|
17
|
-
name: "HealthPluginPlugin",
|
|
18
|
-
dependencies: [
|
|
19
|
-
.product(name: "Capacitor", package: "capacitor-swift-pm"),
|
|
20
|
-
.product(name: "Cordova", package: "capacitor-swift-pm")
|
|
21
|
-
],
|
|
22
|
-
path: "ios/Sources/HealthPluginPlugin"),
|
|
23
|
-
.testTarget(
|
|
24
|
-
name: "HealthPluginPluginTests",
|
|
25
|
-
dependencies: ["HealthPluginPlugin"],
|
|
26
|
-
path: "ios/Tests/HealthPluginPluginTests")
|
|
27
|
-
]
|
|
28
|
-
)
|