@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&lt;<a href="#querylatestsampleresponse">QueryLatestSampleResponse</a>&gt;</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&lt;<a href="#querylatestsampleresponse">QueryLatestSampleResponse</a>&gt;</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&lt;<a href="#querylatestsampleresponse">QueryLatestSampleResponse</a>&gt;</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&lt;<a href="#querylatestsampleresponse">QueryLatestSampleResponse</a>&gt;</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&lt;<a href="#querylatestsampleresponse">QueryLatestSampleResponse</a>&gt;</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("Samples count for \(dataTypeString):", samples?.count ?? 0)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flomentumsolutions/capacitor-health-extended",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
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",