@flomentumsolutions/capacitor-health-extended 0.0.10 → 0.0.12
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
|
@@ -112,6 +112,11 @@ This setup ensures your WebView will load HTTPS content securely and complies wi
|
|
|
112
112
|
* [`showHealthConnectInPlayStore()`](#showhealthconnectinplaystore)
|
|
113
113
|
* [`queryAggregated(...)`](#queryaggregated)
|
|
114
114
|
* [`queryWorkouts(...)`](#queryworkouts)
|
|
115
|
+
* [`queryLatestSample(...)`](#querylatestsample)
|
|
116
|
+
* [`queryWeight()`](#queryweight)
|
|
117
|
+
* [`queryHeight()`](#queryheight)
|
|
118
|
+
* [`queryHeartRate()`](#queryheartrate)
|
|
119
|
+
* [`querySteps()`](#querysteps)
|
|
115
120
|
* [Interfaces](#interfaces)
|
|
116
121
|
* [Type Aliases](#type-aliases)
|
|
117
122
|
|
|
@@ -245,6 +250,75 @@ Query workouts
|
|
|
245
250
|
--------------------
|
|
246
251
|
|
|
247
252
|
|
|
253
|
+
### queryLatestSample(...)
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
queryLatestSample(request: { dataType: string; }) => Promise<QueryLatestSampleResponse>
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Query latest sample for a specific data type
|
|
260
|
+
|
|
261
|
+
| Param | Type |
|
|
262
|
+
| ------------- | ---------------------------------- |
|
|
263
|
+
| **`request`** | <code>{ dataType: string; }</code> |
|
|
264
|
+
|
|
265
|
+
**Returns:** <code>Promise<<a href="#querylatestsampleresponse">QueryLatestSampleResponse</a>></code>
|
|
266
|
+
|
|
267
|
+
--------------------
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
### queryWeight()
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
queryWeight() => Promise<QueryLatestSampleResponse>
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Query latest weight sample
|
|
277
|
+
|
|
278
|
+
**Returns:** <code>Promise<<a href="#querylatestsampleresponse">QueryLatestSampleResponse</a>></code>
|
|
279
|
+
|
|
280
|
+
--------------------
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
### queryHeight()
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
queryHeight() => Promise<QueryLatestSampleResponse>
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Query latest height sample
|
|
290
|
+
|
|
291
|
+
**Returns:** <code>Promise<<a href="#querylatestsampleresponse">QueryLatestSampleResponse</a>></code>
|
|
292
|
+
|
|
293
|
+
--------------------
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
### queryHeartRate()
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
queryHeartRate() => Promise<QueryLatestSampleResponse>
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Query latest heart rate sample
|
|
303
|
+
|
|
304
|
+
**Returns:** <code>Promise<<a href="#querylatestsampleresponse">QueryLatestSampleResponse</a>></code>
|
|
305
|
+
|
|
306
|
+
--------------------
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
### querySteps()
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
querySteps() => Promise<QueryLatestSampleResponse>
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Query latest steps sample
|
|
316
|
+
|
|
317
|
+
**Returns:** <code>Promise<<a href="#querylatestsampleresponse">QueryLatestSampleResponse</a>></code>
|
|
318
|
+
|
|
319
|
+
--------------------
|
|
320
|
+
|
|
321
|
+
|
|
248
322
|
### Interfaces
|
|
249
323
|
|
|
250
324
|
|
|
@@ -342,6 +416,17 @@ Query workouts
|
|
|
342
416
|
| **`includeSteps`** | <code>boolean</code> |
|
|
343
417
|
|
|
344
418
|
|
|
419
|
+
#### QueryLatestSampleResponse
|
|
420
|
+
|
|
421
|
+
| Prop | Type |
|
|
422
|
+
| --------------- | ------------------- |
|
|
423
|
+
| **`value`** | <code>number</code> |
|
|
424
|
+
| **`systolic`** | <code>number</code> |
|
|
425
|
+
| **`diastolic`** | <code>number</code> |
|
|
426
|
+
| **`timestamp`** | <code>number</code> |
|
|
427
|
+
| **`unit`** | <code>string</code> |
|
|
428
|
+
|
|
429
|
+
|
|
345
430
|
### Type Aliases
|
|
346
431
|
|
|
347
432
|
|
|
@@ -33,7 +33,7 @@ import androidx.core.net.toUri
|
|
|
33
33
|
|
|
34
34
|
enum class CapHealthPermission {
|
|
35
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
|
+
, READ_HRV, READ_BLOOD_PRESSURE, READ_HEIGHT, READ_ROUTE, READ_MINDFULNESS;
|
|
37
37
|
|
|
38
38
|
companion object {
|
|
39
39
|
fun from(s: String): CapHealthPermission? {
|
|
@@ -58,7 +58,9 @@ enum class CapHealthPermission {
|
|
|
58
58
|
Permission(alias = "READ_TOTAL_CALORIES", strings = ["android.permission.health.READ_TOTAL_CALORIES_BURNED"]),
|
|
59
59
|
Permission(alias = "READ_HEART_RATE", strings = ["android.permission.health.READ_HEART_RATE"]),
|
|
60
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"])
|
|
61
|
+
Permission(alias = "READ_BLOOD_PRESSURE", strings = ["android.permission.health.READ_BLOOD_PRESSURE"]),
|
|
62
|
+
Permission(alias = "READ_ROUTE", strings = ["android.permission.health.READ_EXERCISE"]),
|
|
63
|
+
Permission(alias = "READ_MINDFULNESS", strings = ["android.permission.health.READ_SLEEP"])
|
|
62
64
|
]
|
|
63
65
|
)
|
|
64
66
|
|
|
@@ -76,12 +78,15 @@ class HealthPlugin : Plugin() {
|
|
|
76
78
|
CapHealthPermission.READ_STEPS to HealthPermission.getReadPermission(StepsRecord::class),
|
|
77
79
|
CapHealthPermission.READ_HEART_RATE to HealthPermission.getReadPermission(HeartRateRecord::class),
|
|
78
80
|
CapHealthPermission.READ_WEIGHT to HealthPermission.getReadPermission(WeightRecord::class),
|
|
81
|
+
CapHealthPermission.READ_HEIGHT to HealthPermission.getReadPermission(HeightRecord::class),
|
|
79
82
|
CapHealthPermission.READ_ACTIVE_CALORIES to HealthPermission.getReadPermission(ActiveCaloriesBurnedRecord::class),
|
|
80
83
|
CapHealthPermission.READ_TOTAL_CALORIES to HealthPermission.getReadPermission(TotalCaloriesBurnedRecord::class),
|
|
81
84
|
CapHealthPermission.READ_DISTANCE to HealthPermission.getReadPermission(DistanceRecord::class),
|
|
82
85
|
CapHealthPermission.READ_WORKOUTS to HealthPermission.getReadPermission(ExerciseSessionRecord::class),
|
|
83
86
|
CapHealthPermission.READ_HRV to HealthPermission.getReadPermission(HeartRateVariabilitySdnnRecord::class),
|
|
84
|
-
CapHealthPermission.READ_BLOOD_PRESSURE to HealthPermission.getReadPermission(BloodPressureRecord::class)
|
|
87
|
+
CapHealthPermission.READ_BLOOD_PRESSURE to HealthPermission.getReadPermission(BloodPressureRecord::class),
|
|
88
|
+
CapHealthPermission.READ_ROUTE to HealthPermission.getReadPermission(ExerciseSessionRecord::class),
|
|
89
|
+
CapHealthPermission.READ_MINDFULNESS to HealthPermission.getReadPermission(SleepSessionRecord::class)
|
|
85
90
|
)
|
|
86
91
|
|
|
87
92
|
override fun load() {
|
|
@@ -239,6 +244,12 @@ class HealthPlugin : Plugin() {
|
|
|
239
244
|
}
|
|
240
245
|
}
|
|
241
246
|
|
|
247
|
+
// Alias for iOS compatibility
|
|
248
|
+
@PluginMethod
|
|
249
|
+
fun openAppleHealthSettings(call: PluginCall) {
|
|
250
|
+
openHealthConnectSettings(call)
|
|
251
|
+
}
|
|
252
|
+
|
|
242
253
|
// Open the Google Play Store to install Health Connect
|
|
243
254
|
@PluginMethod
|
|
244
255
|
fun showHealthConnectInPlayStore(call: PluginCall) {
|
|
@@ -283,9 +294,13 @@ class HealthPlugin : Plugin() {
|
|
|
283
294
|
val result = when (dataType) {
|
|
284
295
|
"heart-rate" -> readLatestHeartRate()
|
|
285
296
|
"weight" -> readLatestWeight()
|
|
297
|
+
"height" -> readLatestHeight()
|
|
286
298
|
"steps" -> readLatestSteps()
|
|
287
299
|
"hrv" -> readLatestHrv()
|
|
288
300
|
"blood-pressure" -> readLatestBloodPressure()
|
|
301
|
+
"distance" -> readLatestDistance()
|
|
302
|
+
"active-calories" -> readLatestActiveCalories()
|
|
303
|
+
"total-calories" -> readLatestTotalCalories()
|
|
289
304
|
else -> {
|
|
290
305
|
call.reject("Unsupported data type: $dataType")
|
|
291
306
|
return@launch
|
|
@@ -299,6 +314,31 @@ class HealthPlugin : Plugin() {
|
|
|
299
314
|
}
|
|
300
315
|
}
|
|
301
316
|
|
|
317
|
+
// Convenience methods for specific data types
|
|
318
|
+
@PluginMethod
|
|
319
|
+
fun queryWeight(call: PluginCall) {
|
|
320
|
+
call.put("dataType", "weight")
|
|
321
|
+
queryLatestSample(call)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
@PluginMethod
|
|
325
|
+
fun queryHeight(call: PluginCall) {
|
|
326
|
+
call.put("dataType", "height")
|
|
327
|
+
queryLatestSample(call)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
@PluginMethod
|
|
331
|
+
fun queryHeartRate(call: PluginCall) {
|
|
332
|
+
call.put("dataType", "heart-rate")
|
|
333
|
+
queryLatestSample(call)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
@PluginMethod
|
|
337
|
+
fun querySteps(call: PluginCall) {
|
|
338
|
+
call.put("dataType", "steps")
|
|
339
|
+
queryLatestSample(call)
|
|
340
|
+
}
|
|
341
|
+
|
|
302
342
|
private suspend fun readLatestHeartRate(): JSObject {
|
|
303
343
|
if (!hasPermission(CapHealthPermission.READ_HEART_RATE)) {
|
|
304
344
|
throw Exception("Permission for heart rate not granted")
|
|
@@ -313,8 +353,9 @@ class HealthPlugin : Plugin() {
|
|
|
313
353
|
|
|
314
354
|
val lastSample = record.samples.lastOrNull()
|
|
315
355
|
return JSObject().apply {
|
|
316
|
-
put("timestamp", lastSample?.time?.toString() ?: "")
|
|
317
356
|
put("value", lastSample?.beatsPerMinute ?: 0)
|
|
357
|
+
put("timestamp", (lastSample?.time?.epochSecond ?: 0) * 1000) // Convert to milliseconds like iOS
|
|
358
|
+
put("unit", "count/min")
|
|
318
359
|
}
|
|
319
360
|
}
|
|
320
361
|
|
|
@@ -330,8 +371,9 @@ class HealthPlugin : Plugin() {
|
|
|
330
371
|
val result = healthConnectClient.readRecords(request)
|
|
331
372
|
val record = result.records.firstOrNull() ?: throw Exception("No weight data found")
|
|
332
373
|
return JSObject().apply {
|
|
333
|
-
put("timestamp", record.time.toString())
|
|
334
374
|
put("value", record.weight.inKilograms)
|
|
375
|
+
put("timestamp", record.time.epochSecond * 1000) // Convert to milliseconds like iOS
|
|
376
|
+
put("unit", "kg")
|
|
335
377
|
}
|
|
336
378
|
}
|
|
337
379
|
|
|
@@ -347,9 +389,9 @@ class HealthPlugin : Plugin() {
|
|
|
347
389
|
val result = healthConnectClient.readRecords(request)
|
|
348
390
|
val record = result.records.firstOrNull() ?: throw Exception("No step data found")
|
|
349
391
|
return JSObject().apply {
|
|
350
|
-
put("startDate", record.startTime.toString())
|
|
351
|
-
put("endDate", record.endTime.toString())
|
|
352
392
|
put("value", record.count)
|
|
393
|
+
put("timestamp", record.endTime.epochSecond * 1000) // Convert to milliseconds like iOS
|
|
394
|
+
put("unit", "count")
|
|
353
395
|
}
|
|
354
396
|
}
|
|
355
397
|
|
|
@@ -365,8 +407,9 @@ class HealthPlugin : Plugin() {
|
|
|
365
407
|
val result = healthConnectClient.readRecords(request)
|
|
366
408
|
val record = result.records.firstOrNull() ?: throw Exception("No HRV data found")
|
|
367
409
|
return JSObject().apply {
|
|
368
|
-
put("timestamp", record.time.toString())
|
|
369
410
|
put("value", record.sdnnMillis)
|
|
411
|
+
put("timestamp", record.time.epochSecond * 1000) // Convert to milliseconds like iOS
|
|
412
|
+
put("unit", "ms")
|
|
370
413
|
}
|
|
371
414
|
}
|
|
372
415
|
|
|
@@ -382,9 +425,82 @@ class HealthPlugin : Plugin() {
|
|
|
382
425
|
val result = healthConnectClient.readRecords(request)
|
|
383
426
|
val record = result.records.firstOrNull() ?: throw Exception("No blood pressure data found")
|
|
384
427
|
return JSObject().apply {
|
|
385
|
-
put("timestamp", record.time.toString())
|
|
386
428
|
put("systolic", record.systolic.inMillimetersOfMercury)
|
|
387
429
|
put("diastolic", record.diastolic.inMillimetersOfMercury)
|
|
430
|
+
put("timestamp", record.time.epochSecond * 1000) // Convert to milliseconds like iOS
|
|
431
|
+
put("unit", "mmHg")
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
private suspend fun readLatestHeight(): JSObject {
|
|
436
|
+
if (!hasPermission(CapHealthPermission.READ_HEIGHT)) {
|
|
437
|
+
throw Exception("Permission for height not granted")
|
|
438
|
+
}
|
|
439
|
+
val request = ReadRecordsRequest(
|
|
440
|
+
recordType = HeightRecord::class,
|
|
441
|
+
timeRangeFilter = TimeRangeFilter.after(Instant.EPOCH),
|
|
442
|
+
pageSize = 1
|
|
443
|
+
)
|
|
444
|
+
val result = healthConnectClient.readRecords(request)
|
|
445
|
+
val record = result.records.firstOrNull() ?: throw Exception("No height data found")
|
|
446
|
+
return JSObject().apply {
|
|
447
|
+
put("value", record.height.inMeters)
|
|
448
|
+
put("timestamp", record.time.epochSecond * 1000) // Convert to milliseconds like iOS
|
|
449
|
+
put("unit", "m")
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
private suspend fun readLatestDistance(): JSObject {
|
|
454
|
+
if (!hasPermission(CapHealthPermission.READ_DISTANCE)) {
|
|
455
|
+
throw Exception("Permission for distance not granted")
|
|
456
|
+
}
|
|
457
|
+
val request = ReadRecordsRequest(
|
|
458
|
+
recordType = DistanceRecord::class,
|
|
459
|
+
timeRangeFilter = TimeRangeFilter.after(Instant.EPOCH),
|
|
460
|
+
pageSize = 1
|
|
461
|
+
)
|
|
462
|
+
val result = healthConnectClient.readRecords(request)
|
|
463
|
+
val record = result.records.firstOrNull() ?: throw Exception("No distance data found")
|
|
464
|
+
return JSObject().apply {
|
|
465
|
+
put("value", record.distance.inMeters)
|
|
466
|
+
put("timestamp", record.time.epochSecond * 1000) // Convert to milliseconds like iOS
|
|
467
|
+
put("unit", "m")
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
private suspend fun readLatestActiveCalories(): JSObject {
|
|
472
|
+
if (!hasPermission(CapHealthPermission.READ_ACTIVE_CALORIES)) {
|
|
473
|
+
throw Exception("Permission for active calories not granted")
|
|
474
|
+
}
|
|
475
|
+
val request = ReadRecordsRequest(
|
|
476
|
+
recordType = ActiveCaloriesBurnedRecord::class,
|
|
477
|
+
timeRangeFilter = TimeRangeFilter.after(Instant.EPOCH),
|
|
478
|
+
pageSize = 1
|
|
479
|
+
)
|
|
480
|
+
val result = healthConnectClient.readRecords(request)
|
|
481
|
+
val record = result.records.firstOrNull() ?: throw Exception("No active calories data found")
|
|
482
|
+
return JSObject().apply {
|
|
483
|
+
put("value", record.activeCalories.inKilocalories)
|
|
484
|
+
put("timestamp", record.time.epochSecond * 1000) // Convert to milliseconds like iOS
|
|
485
|
+
put("unit", "kcal")
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private suspend fun readLatestTotalCalories(): JSObject {
|
|
490
|
+
if (!hasPermission(CapHealthPermission.READ_TOTAL_CALORIES)) {
|
|
491
|
+
throw Exception("Permission for total calories not granted")
|
|
492
|
+
}
|
|
493
|
+
val request = ReadRecordsRequest(
|
|
494
|
+
recordType = TotalCaloriesBurnedRecord::class,
|
|
495
|
+
timeRangeFilter = TimeRangeFilter.after(Instant.EPOCH),
|
|
496
|
+
pageSize = 1
|
|
497
|
+
)
|
|
498
|
+
val result = healthConnectClient.readRecords(request)
|
|
499
|
+
val record = result.records.firstOrNull() ?: throw Exception("No total calories data found")
|
|
500
|
+
return JSObject().apply {
|
|
501
|
+
put("value", record.energy.inKilocalories)
|
|
502
|
+
put("timestamp", record.time.epochSecond * 1000) // Convert to milliseconds like iOS
|
|
503
|
+
put("unit", "kcal")
|
|
388
504
|
}
|
|
389
505
|
}
|
|
390
506
|
|
|
@@ -50,6 +50,29 @@ export interface HealthPlugin {
|
|
|
50
50
|
* @param request
|
|
51
51
|
*/
|
|
52
52
|
queryWorkouts(request: QueryWorkoutRequest): Promise<QueryWorkoutResponse>;
|
|
53
|
+
/**
|
|
54
|
+
* Query latest sample for a specific data type
|
|
55
|
+
* @param request
|
|
56
|
+
*/
|
|
57
|
+
queryLatestSample(request: {
|
|
58
|
+
dataType: string;
|
|
59
|
+
}): Promise<QueryLatestSampleResponse>;
|
|
60
|
+
/**
|
|
61
|
+
* Query latest weight sample
|
|
62
|
+
*/
|
|
63
|
+
queryWeight(): Promise<QueryLatestSampleResponse>;
|
|
64
|
+
/**
|
|
65
|
+
* Query latest height sample
|
|
66
|
+
*/
|
|
67
|
+
queryHeight(): Promise<QueryLatestSampleResponse>;
|
|
68
|
+
/**
|
|
69
|
+
* Query latest heart rate sample
|
|
70
|
+
*/
|
|
71
|
+
queryHeartRate(): Promise<QueryLatestSampleResponse>;
|
|
72
|
+
/**
|
|
73
|
+
* Query latest steps sample
|
|
74
|
+
*/
|
|
75
|
+
querySteps(): Promise<QueryLatestSampleResponse>;
|
|
53
76
|
}
|
|
54
77
|
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
78
|
export interface PermissionsRequest {
|
|
@@ -108,3 +131,10 @@ export interface AggregatedSample {
|
|
|
108
131
|
endDate: string;
|
|
109
132
|
value: number;
|
|
110
133
|
}
|
|
134
|
+
export interface QueryLatestSampleResponse {
|
|
135
|
+
value?: number;
|
|
136
|
+
systolic?: number;
|
|
137
|
+
diastolic?: number;
|
|
138
|
+
timestamp: number;
|
|
139
|
+
unit: string;
|
|
140
|
+
}
|
|
@@ -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_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"]}
|
|
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 /**\n * Query latest sample for a specific data type\n * @param request\n */\n queryLatestSample(request: { dataType: string }): 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\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\nexport interface QueryLatestSampleResponse {\n value?: number;\n systolic?: number;\n diastolic?: number;\n timestamp: number;\n unit: string;\n}\n"]}
|
|
@@ -17,7 +17,11 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
17
17
|
CAPPluginMethod(name: "openAppleHealthSettings", returnType: CAPPluginReturnPromise),
|
|
18
18
|
CAPPluginMethod(name: "queryAggregated", returnType: CAPPluginReturnPromise),
|
|
19
19
|
CAPPluginMethod(name: "queryWorkouts", returnType: CAPPluginReturnPromise),
|
|
20
|
-
CAPPluginMethod(name: "queryLatestSample", returnType: CAPPluginReturnPromise)
|
|
20
|
+
CAPPluginMethod(name: "queryLatestSample", returnType: CAPPluginReturnPromise),
|
|
21
|
+
CAPPluginMethod(name: "queryWeight", returnType: CAPPluginReturnPromise),
|
|
22
|
+
CAPPluginMethod(name: "queryHeight", returnType: CAPPluginReturnPromise),
|
|
23
|
+
CAPPluginMethod(name: "queryHeartRate", returnType: CAPPluginReturnPromise),
|
|
24
|
+
CAPPluginMethod(name: "querySteps", returnType: CAPPluginReturnPromise)
|
|
21
25
|
]
|
|
22
26
|
|
|
23
27
|
let healthStore = HKHealthStore()
|
|
@@ -65,8 +69,19 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
65
69
|
return
|
|
66
70
|
}
|
|
67
71
|
|
|
72
|
+
print("⚡️ [HealthPlugin] Requesting permissions: \(permissions)")
|
|
73
|
+
|
|
68
74
|
let types: [HKObjectType] = permissions.flatMap { permissionToHKObjectType($0) }
|
|
69
75
|
|
|
76
|
+
print("⚡️ [HealthPlugin] Mapped to \(types.count) HKObjectTypes")
|
|
77
|
+
|
|
78
|
+
// Validate that we have at least one valid permission type
|
|
79
|
+
guard !types.isEmpty else {
|
|
80
|
+
let invalidPermissions = permissions.filter { permissionToHKObjectType($0).isEmpty }
|
|
81
|
+
call.reject("No valid permission types found. Invalid permissions: \(invalidPermissions)")
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
70
85
|
healthStore.requestAuthorization(toShare: nil, read: Set(types)) { success, error in
|
|
71
86
|
if success {
|
|
72
87
|
//we don't know which actual permissions were granted, so we assume all
|
|
@@ -89,6 +104,8 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
89
104
|
call.reject("Missing data type")
|
|
90
105
|
return
|
|
91
106
|
}
|
|
107
|
+
|
|
108
|
+
print("⚡️ [HealthPlugin] Querying latest sample for data type: \(dataTypeString)")
|
|
92
109
|
// ---- Special handling for blood‑pressure correlation ----
|
|
93
110
|
if dataTypeString == "blood-pressure" {
|
|
94
111
|
guard let bpType = HKObjectType.correlationType(forIdentifier: .bloodPressure) else {
|
|
@@ -176,12 +193,14 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
176
193
|
|
|
177
194
|
let query = HKSampleQuery(sampleType: type, predicate: predicate, limit: 1, sortDescriptors: [sortDescriptor]) { _, samples, error in
|
|
178
195
|
|
|
179
|
-
print("
|
|
196
|
+
print("⚡️ [HealthPlugin] Query completed for \(dataTypeString): \(samples?.count ?? 0) samples, error: \(error?.localizedDescription ?? "none")")
|
|
180
197
|
|
|
181
198
|
guard let quantitySample = samples?.first as? HKQuantitySample else {
|
|
182
199
|
if let error = error {
|
|
200
|
+
print("⚡️ [HealthPlugin] Error fetching \(dataTypeString): \(error.localizedDescription)")
|
|
183
201
|
call.reject("Error fetching latest sample", "NO_SAMPLE", error)
|
|
184
202
|
} else {
|
|
203
|
+
print("⚡️ [HealthPlugin] No sample found for \(dataTypeString)")
|
|
185
204
|
call.reject("No sample found", "NO_SAMPLE")
|
|
186
205
|
}
|
|
187
206
|
return
|
|
@@ -204,6 +223,8 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
204
223
|
let value = quantitySample.quantity.doubleValue(for: unit)
|
|
205
224
|
let timestamp = quantitySample.startDate.timeIntervalSince1970 * 1000
|
|
206
225
|
|
|
226
|
+
print("⚡️ [HealthPlugin] Successfully fetched \(dataTypeString): value=\(value), unit=\(unit.unitString)")
|
|
227
|
+
|
|
207
228
|
call.resolve([
|
|
208
229
|
"value": value,
|
|
209
230
|
"timestamp": timestamp,
|
|
@@ -214,6 +235,27 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
214
235
|
healthStore.execute(query)
|
|
215
236
|
}
|
|
216
237
|
|
|
238
|
+
// Convenience methods for specific data types
|
|
239
|
+
@objc func queryWeight(_ call: CAPPluginCall) {
|
|
240
|
+
call.setString("dataType", "weight")
|
|
241
|
+
queryLatestSample(call)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
@objc func queryHeight(_ call: CAPPluginCall) {
|
|
245
|
+
call.setString("dataType", "height")
|
|
246
|
+
queryLatestSample(call)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
@objc func queryHeartRate(_ call: CAPPluginCall) {
|
|
250
|
+
call.setString("dataType", "heart-rate")
|
|
251
|
+
queryLatestSample(call)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
@objc func querySteps(_ call: CAPPluginCall) {
|
|
255
|
+
call.setString("dataType", "steps")
|
|
256
|
+
queryLatestSample(call)
|
|
257
|
+
}
|
|
258
|
+
|
|
217
259
|
@objc func openAppleHealthSettings(_ call: CAPPluginCall) {
|
|
218
260
|
if let url = URL(string: UIApplication.openSettingsURLString) {
|
|
219
261
|
DispatchQueue.main.async {
|
|
@@ -263,7 +305,44 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
263
305
|
HKObjectType.quantityType(forIdentifier: .bloodPressureSystolic),
|
|
264
306
|
HKObjectType.quantityType(forIdentifier: .bloodPressureDiastolic)
|
|
265
307
|
].compactMap { $0 }
|
|
308
|
+
// Add common alternative permission names
|
|
309
|
+
case "steps":
|
|
310
|
+
return [HKObjectType.quantityType(forIdentifier: .stepCount)].compactMap{$0}
|
|
311
|
+
case "weight":
|
|
312
|
+
return [HKObjectType.quantityType(forIdentifier: .bodyMass)].compactMap{$0}
|
|
313
|
+
case "height":
|
|
314
|
+
return [HKObjectType.quantityType(forIdentifier: .height)].compactMap { $0 }
|
|
315
|
+
case "calories", "total-calories":
|
|
316
|
+
return [
|
|
317
|
+
HKObjectType.quantityType(forIdentifier: .activeEnergyBurned),
|
|
318
|
+
HKObjectType.quantityType(forIdentifier: .basalEnergyBurned) // iOS 16+
|
|
319
|
+
].compactMap { $0 }
|
|
320
|
+
case "active-calories":
|
|
321
|
+
return [HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)].compactMap{$0}
|
|
322
|
+
case "workouts":
|
|
323
|
+
return [HKObjectType.workoutType()].compactMap{$0}
|
|
324
|
+
case "heart-rate", "heartrate", "heart_rate":
|
|
325
|
+
return [HKObjectType.quantityType(forIdentifier: .heartRate)].compactMap{$0}
|
|
326
|
+
case "route":
|
|
327
|
+
return [HKSeriesType.workoutRoute()].compactMap{$0}
|
|
328
|
+
case "distance":
|
|
329
|
+
return [
|
|
330
|
+
HKObjectType.quantityType(forIdentifier: .distanceCycling),
|
|
331
|
+
HKObjectType.quantityType(forIdentifier: .distanceSwimming),
|
|
332
|
+
HKObjectType.quantityType(forIdentifier: .distanceWalkingRunning),
|
|
333
|
+
HKObjectType.quantityType(forIdentifier: .distanceDownhillSnowSports)
|
|
334
|
+
].compactMap{$0}
|
|
335
|
+
case "mindfulness":
|
|
336
|
+
return [HKObjectType.categoryType(forIdentifier: .mindfulSession)!].compactMap{$0}
|
|
337
|
+
case "hrv", "heart_rate_variability_sdnn":
|
|
338
|
+
return [HKObjectType.quantityType(forIdentifier: .heartRateVariabilitySDNN)].compactMap { $0 }
|
|
339
|
+
case "blood-pressure", "bloodpressure", "blood_pressure_systolic", "blood_pressure_diastolic":
|
|
340
|
+
return [
|
|
341
|
+
HKObjectType.quantityType(forIdentifier: .bloodPressureSystolic),
|
|
342
|
+
HKObjectType.quantityType(forIdentifier: .bloodPressureDiastolic)
|
|
343
|
+
].compactMap { $0 }
|
|
266
344
|
default:
|
|
345
|
+
print("⚡️ [HealthPlugin] Unknown permission: \(permission)")
|
|
267
346
|
return []
|
|
268
347
|
}
|
|
269
348
|
}
|
package/package.json
CHANGED