@flomentumsolutions/capacitor-health-extended 0.4.5 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -28,12 +28,14 @@ Thanks [@mley](https://github.com/mley) for the ground work. The goal of this fo
28
28
  - Query aggregated data like steps or calories
29
29
  - Retrieve workout sessions with optional route and heart rate data
30
30
  - Fetch the latest samples for steps, distance (incl. cycling), calories (active/total/basal), heart‑rate, resting HR, HRV, respiratory rate, blood pressure, oxygen saturation, blood glucose, body temperature (basal + core), body fat, height, weight, flights climbed, sleep, and exercise time.
31
+ - Read profile characteristics on iOS: biological sex, blood type, date of birth, Fitzpatrick skin type, wheelchair use.
31
32
 
32
33
  ### Supported data types (parity iOS + Android)
33
34
  - Activity: steps, distance, distance‑cycling, exercise time (Apple Exercise Time), workouts (with routes/steps/calories), flights climbed
34
35
  - Energy: active calories, total calories, basal calories
35
36
  - Vitals: heart rate, resting heart rate, HRV, respiratory rate, blood pressure, oxygen saturation, blood glucose, body temperature, basal body temperature
36
37
  - Body: weight, height, body fat
38
+ - Characteristics (iOS): biological sex, blood type, date of birth, Fitzpatrick skin type, wheelchair use
37
39
  - Sessions: mindfulness, sleep
38
40
 
39
41
  ## Install
@@ -213,6 +215,7 @@ This setup ensures your WebView will load HTTPS content securely and complies wi
213
215
  * [`openAppleHealthSettings()`](#openapplehealthsettings)
214
216
  * [`openHealthConnectSettings()`](#openhealthconnectsettings)
215
217
  * [`showHealthConnectInPlayStore()`](#showhealthconnectinplaystore)
218
+ * [`getCharacteristics()`](#getcharacteristics)
216
219
  * [`queryAggregated(...)`](#queryaggregated)
217
220
  * [`queryWorkouts(...)`](#queryworkouts)
218
221
  * [`queryLatestSample(...)`](#querylatestsample)
@@ -319,20 +322,27 @@ Opens the Google Health Connect app in PlayStore
319
322
  --------------------
320
323
 
321
324
 
322
- ### queryAggregated(...)
325
+ ### getCharacteristics()
323
326
 
324
327
  ```typescript
325
- queryAggregated(request: QueryAggregatedRequest) => Promise<QueryAggregatedResponse>
328
+ getCharacteristics() => Promise<CharacteristicsResponse>
326
329
  ```
327
330
 
328
- Query aggregated data
331
+ iOS only: Reads user characteristics such as biological sex, blood type, date of birth, Fitzpatrick skin type, and wheelchair use.
332
+ Values are null when unavailable or permission was not granted. Android does not expose these characteristics; it returns `platformSupported: false` and a `platformMessage` for UI hints without emitting null values.
329
333
 
330
- | Param | Type |
331
- | ------------- | ------------------------------------------------------------------------- |
332
- | **`request`** | <code><a href="#queryaggregatedrequest">QueryAggregatedRequest</a></code> |
334
+ **Returns:** <code>Promise&lt;<a href="#characteristicsresponse">CharacteristicsResponse</a>&gt;</code>
333
335
 
334
- **Returns:** <code>Promise&lt;<a href="#queryaggregatedresponse">QueryAggregatedResponse</a>&gt;</code>
336
+ --------------------
337
+
338
+
339
+ ### queryAggregated(...)
340
+
341
+ ```typescript
342
+ queryAggregated(request: QueryAggregatedRequest) => Promise<QueryAggregatedResponse>
343
+ ```
335
344
 
345
+ Query aggregated data
336
346
  - Blood-pressure aggregates return the systolic average in `value` plus `systolic`, `diastolic`, and `unit`.
337
347
  - `total-calories` is derived as active + basal energy on both iOS and Android for latest samples, aggregated queries, and workouts. We fall back to the platform's total‑calories metric (or active calories) when basal data isn't available or permission is missing. Request both `READ_ACTIVE_CALORIES` and `READ_BASAL_CALORIES` for full totals.
338
348
  - Weight/height aggregation returns the latest sample per day (no averaging).
@@ -340,6 +350,12 @@ Query aggregated data
340
350
  - Android `distance-cycling` aggregates distance recorded during biking exercise sessions (requires distance + workouts permissions).
341
351
  - Daily `bucket: "day"` queries use calendar-day boundaries in the device time zone (start-of-day through the next start-of-day) instead of a trailing 24-hour window. For “today,” send `startDate` at today’s start-of-day and `endDate` at now or tomorrow’s start-of-day.
342
352
 
353
+ | Param | Type |
354
+ | ------------- | ------------------------------------------------------------------------- |
355
+ | **`request`** | <code><a href="#queryaggregatedrequest">QueryAggregatedRequest</a></code> |
356
+
357
+ **Returns:** <code>Promise&lt;<a href="#queryaggregatedresponse">QueryAggregatedResponse</a>&gt;</code>
358
+
343
359
  --------------------
344
360
 
345
361
 
@@ -363,19 +379,18 @@ Query workouts
363
379
  ### queryLatestSample(...)
364
380
 
365
381
  ```typescript
366
- queryLatestSample(request: { dataType: string; }) => Promise<QueryLatestSampleResponse>
382
+ queryLatestSample(request: { dataType: LatestDataType; }) => Promise<QueryLatestSampleResponse>
367
383
  ```
368
384
 
369
385
  Query latest sample for a specific data type
386
+ - Latest sleep sample returns the most recent complete sleep session (asleep states only) from the last ~36 hours; if a longer overnight session exists, shorter naps are ignored.
370
387
 
371
- | Param | Type |
372
- | ------------- | ---------------------------------- |
373
- | **`request`** | <code>{ dataType: string; }</code> |
388
+ | Param | Type |
389
+ | ------------- | ------------------------------------------------------------------------ |
390
+ | **`request`** | <code>{ dataType: <a href="#latestdatatype">LatestDataType</a>; }</code> |
374
391
 
375
392
  **Returns:** <code>Promise&lt;<a href="#querylatestsampleresponse">QueryLatestSampleResponse</a>&gt;</code>
376
393
 
377
- - Latest sleep sample returns the most recent complete sleep session (asleep states only) from the last ~36 hours; if a longer overnight session exists, shorter naps are ignored.
378
-
379
394
  --------------------
380
395
 
381
396
 
@@ -436,9 +451,9 @@ Query latest steps sample
436
451
 
437
452
  #### PermissionResponse
438
453
 
439
- | Prop | Type |
440
- | ----------------- | ------------------------------------------ |
441
- | **`permissions`** | <code>Record<HealthPermission, boolean></code> |
454
+ | Prop | Type |
455
+ | ----------------- | ---------------------------------------------------------------------------------------------------------- |
456
+ | **`permissions`** | <code><a href="#record">Record</a>&lt;<a href="#healthpermission">HealthPermission</a>, boolean&gt;</code> |
442
457
 
443
458
 
444
459
  #### PermissionsRequest
@@ -448,6 +463,19 @@ Query latest steps sample
448
463
  | **`permissions`** | <code>HealthPermission[]</code> |
449
464
 
450
465
 
466
+ #### CharacteristicsResponse
467
+
468
+ | Prop | Type | Description |
469
+ | ------------------------- | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
470
+ | **`biologicalSex`** | <code><a href="#healthbiologicalsex">HealthBiologicalSex</a> \| null</code> | |
471
+ | **`bloodType`** | <code><a href="#healthbloodtype">HealthBloodType</a> \| null</code> | |
472
+ | **`dateOfBirth`** | <code>string \| null</code> | |
473
+ | **`fitzpatrickSkinType`** | <code><a href="#healthfitzpatrickskintype">HealthFitzpatrickSkinType</a> \| null</code> | |
474
+ | **`wheelchairUse`** | <code><a href="#healthwheelchairuse">HealthWheelchairUse</a> \| null</code> | |
475
+ | **`platformSupported`** | <code>boolean</code> | Indicates whether the platform exposes these characteristics via the plugin (true on iOS, false on Android). |
476
+ | **`platformMessage`** | <code>string</code> | Optional platform-specific message; on Android we return a user-facing note explaining that values remain empty unless synced from iOS. |
477
+
478
+
451
479
  #### QueryAggregatedResponse
452
480
 
453
481
  | Prop | Type |
@@ -457,14 +485,14 @@ Query latest steps sample
457
485
 
458
486
  #### AggregatedSample
459
487
 
460
- | Prop | Type |
461
- | ----------------- | ------------------- |
462
- | **`startDate`** | <code>string</code> |
463
- | **`endDate`** | <code>string</code> |
464
- | **`value`** | <code>number</code> |
465
- | **`systolic`** | <code>number</code> |
466
- | **`diastolic`** | <code>number</code> |
467
- | **`unit`** | <code>string</code> |
488
+ | Prop | Type |
489
+ | --------------- | ------------------- |
490
+ | **`startDate`** | <code>string</code> |
491
+ | **`endDate`** | <code>string</code> |
492
+ | **`value`** | <code>number</code> |
493
+ | **`systolic`** | <code>number</code> |
494
+ | **`diastolic`** | <code>number</code> |
495
+ | **`unit`** | <code>string</code> |
468
496
 
469
497
 
470
498
  #### QueryAggregatedRequest
@@ -473,7 +501,7 @@ Query latest steps sample
473
501
  | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
474
502
  | **`startDate`** | <code>string</code> |
475
503
  | **`endDate`** | <code>string</code> |
476
- | **`dataType`** | <code>'steps' \| 'active-calories' \| 'total-calories' \| 'basal-calories' \| 'distance' \| 'distance-cycling' \| 'weight' \| 'height' \| 'heart-rate' \| 'resting-heart-rate' \| 'respiratory-rate' \| 'oxygen-saturation' \| 'blood-glucose' \| 'body-temperature' \| 'basal-body-temperature' \| 'body-fat' \| 'flights-climbed' \| 'exercise-time' \| 'sleep' \| 'mindfulness' \| 'hrv' \| 'blood-pressure'</code> |
504
+ | **`dataType`** | <code>'steps' \| 'active-calories' \| 'total-calories' \| 'basal-calories' \| 'distance' \| 'weight' \| 'height' \| 'heart-rate' \| 'resting-heart-rate' \| 'respiratory-rate' \| 'oxygen-saturation' \| 'blood-glucose' \| 'body-temperature' \| 'basal-body-temperature' \| 'body-fat' \| 'flights-climbed' \| 'exercise-time' \| 'distance-cycling' \| 'mindfulness' \| 'sleep' \| 'hrv' \| 'blood-pressure'</code> |
477
505
  | **`bucket`** | <code>string</code> |
478
506
 
479
507
 
@@ -533,20 +561,54 @@ Query latest steps sample
533
561
 
534
562
  #### QueryLatestSampleResponse
535
563
 
536
- | Prop | Type |
537
- | --------------- | ------------------- |
538
- | **`value`** | <code>number</code> |
539
- | **`systolic`** | <code>number</code> |
540
- | **`diastolic`** | <code>number</code> |
541
- | **`timestamp`** | <code>number</code> |
542
- | **`unit`** | <code>string</code> |
564
+ | Prop | Type |
565
+ | ------------------ | ---------------------------------------------------------------- |
566
+ | **`value`** | <code>number</code> |
567
+ | **`systolic`** | <code>number</code> |
568
+ | **`diastolic`** | <code>number</code> |
569
+ | **`timestamp`** | <code>number</code> |
570
+ | **`endTimestamp`** | <code>number</code> |
571
+ | **`unit`** | <code>string</code> |
572
+ | **`metadata`** | <code><a href="#record">Record</a>&lt;string, unknown&gt;</code> |
543
573
 
544
574
 
545
575
  ### Type Aliases
546
576
 
547
577
 
578
+ #### Record
579
+
580
+ Construct a type with a set of properties K of type T
581
+
582
+ <code>{
548
583
  [P in K]: T;
549
584
  }</code>
585
+
586
+
550
587
  #### HealthPermission
551
588
 
552
- <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>
589
+ <code>'READ_STEPS' | 'READ_WORKOUTS' | 'READ_ACTIVE_CALORIES' | 'READ_TOTAL_CALORIES' | 'READ_DISTANCE' | 'READ_WEIGHT' | 'READ_HEIGHT' | 'READ_HEART_RATE' | 'READ_RESTING_HEART_RATE' | 'READ_ROUTE' | 'READ_MINDFULNESS' | 'READ_HRV' | 'READ_BLOOD_PRESSURE' | 'READ_BASAL_CALORIES' | 'READ_RESPIRATORY_RATE' | 'READ_OXYGEN_SATURATION' | 'READ_BLOOD_GLUCOSE' | 'READ_BODY_TEMPERATURE' | 'READ_BASAL_BODY_TEMPERATURE' | 'READ_BODY_FAT' | 'READ_FLOORS_CLIMBED' | 'READ_SLEEP' | 'READ_EXERCISE_TIME' | 'READ_BIOLOGICAL_SEX' | 'READ_BLOOD_TYPE' | 'READ_DATE_OF_BIRTH' | 'READ_FITZPATRICK_SKIN_TYPE' | 'READ_WHEELCHAIR_USE'</code>
590
+
591
+
592
+ #### HealthBiologicalSex
593
+
594
+ <code>'female' | 'male' | 'other' | 'not_set' | 'unknown'</code>
595
+
596
+
597
+ #### HealthBloodType
598
+
599
+ <code>'a-positive' | 'a-negative' | 'b-positive' | 'b-negative' | 'ab-positive' | 'ab-negative' | 'o-positive' | 'o-negative' | 'not_set' | 'unknown'</code>
600
+
601
+
602
+ #### HealthFitzpatrickSkinType
603
+
604
+ <code>'type1' | 'type2' | 'type3' | 'type4' | 'type5' | 'type6' | 'not_set' | 'unknown'</code>
605
+
606
+
607
+ #### HealthWheelchairUse
608
+
609
+ <code>'wheelchair_user' | 'not_wheelchair_user' | 'not_set' | 'unknown'</code>
610
+
611
+
612
+ #### LatestDataType
613
+
614
+ <code>'steps' | 'active-calories' | 'total-calories' | 'basal-calories' | 'distance' | 'weight' | 'height' | 'heart-rate' | 'resting-heart-rate' | 'respiratory-rate' | 'oxygen-saturation' | 'blood-glucose' | 'body-temperature' | 'basal-body-temperature' | 'body-fat' | 'flights-climbed' | 'exercise-time' | 'distance-cycling' | 'mindfulness' | 'sleep' | 'hrv' | 'blood-pressure'</code>
553
615
 
554
616
  </docgen-api>
@@ -313,6 +313,17 @@ class HealthPlugin : Plugin() {
313
313
  call.resolve()
314
314
  }
315
315
 
316
+ @PluginMethod
317
+ fun getCharacteristics(call: PluginCall) {
318
+ val result = JSObject()
319
+ result.put("platformSupported", false)
320
+ result.put(
321
+ "platformMessage",
322
+ "Health Connect does not expose characteristics; this section stays empty unless synced from an iOS device."
323
+ )
324
+ call.resolve(result)
325
+ }
326
+
316
327
  private fun getMetricAndMapper(dataType: String): MetricAndMapper {
317
328
  return when (dataType) {
318
329
  "steps" -> metricAndMapper("steps", CapHealthPermission.READ_STEPS, StepsRecord.COUNT_TOTAL) { it?.toDouble() }
@@ -40,8 +40,19 @@ export interface HealthPlugin {
40
40
  * Opens the Google Health Connect app in PlayStore
41
41
  */
42
42
  showHealthConnectInPlayStore(): Promise<void>;
43
+ /**
44
+ * iOS only: Reads user characteristics such as biological sex, blood type, date of birth, Fitzpatrick skin type, and wheelchair use.
45
+ * Values are null when unavailable or permission was not granted. Android does not expose these characteristics; it returns `platformSupported: false` and a `platformMessage` for UI hints without emitting null values.
46
+ */
47
+ getCharacteristics(): Promise<CharacteristicsResponse>;
43
48
  /**
44
49
  * Query aggregated data
50
+ * - Blood-pressure aggregates return the systolic average in `value` plus `systolic`, `diastolic`, and `unit`.
51
+ * - `total-calories` is derived as active + basal energy on both iOS and Android for latest samples, aggregated queries, and workouts. We fall back to the platform's total‑calories metric (or active calories) when basal data isn't available or permission is missing. Request both `READ_ACTIVE_CALORIES` and `READ_BASAL_CALORIES` for full totals.
52
+ * - Weight/height aggregation returns the latest sample per day (no averaging).
53
+ * - Android aggregation currently supports daily buckets; unsupported buckets will be rejected.
54
+ * - Android `distance-cycling` aggregates distance recorded during biking exercise sessions (requires distance + workouts permissions).
55
+ * - Daily `bucket: "day"` queries use calendar-day boundaries in the device time zone (start-of-day through the next start-of-day) instead of a trailing 24-hour window. For “today,” send `startDate` at today’s start-of-day and `endDate` at now or tomorrow’s start-of-day.
45
56
  * @param request
46
57
  */
47
58
  queryAggregated(request: QueryAggregatedRequest): Promise<QueryAggregatedResponse>;
@@ -52,6 +63,7 @@ export interface HealthPlugin {
52
63
  queryWorkouts(request: QueryWorkoutRequest): Promise<QueryWorkoutResponse>;
53
64
  /**
54
65
  * Query latest sample for a specific data type
66
+ * - Latest sleep sample returns the most recent complete sleep session (asleep states only) from the last ~36 hours; if a longer overnight session exists, shorter naps are ignored.
55
67
  * @param request
56
68
  */
57
69
  queryLatestSample(request: {
@@ -74,7 +86,7 @@ export interface HealthPlugin {
74
86
  */
75
87
  querySteps(): Promise<QueryLatestSampleResponse>;
76
88
  }
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_RESTING_HEART_RATE' | 'READ_ROUTE' | 'READ_MINDFULNESS' | 'READ_HRV' | 'READ_BLOOD_PRESSURE' | 'READ_BASAL_CALORIES' | 'READ_RESPIRATORY_RATE' | 'READ_OXYGEN_SATURATION' | 'READ_BLOOD_GLUCOSE' | 'READ_BODY_TEMPERATURE' | 'READ_BASAL_BODY_TEMPERATURE' | 'READ_BODY_FAT' | 'READ_FLOORS_CLIMBED' | 'READ_SLEEP' | 'READ_EXERCISE_TIME';
89
+ export declare type HealthPermission = 'READ_STEPS' | 'READ_WORKOUTS' | 'READ_ACTIVE_CALORIES' | 'READ_TOTAL_CALORIES' | 'READ_DISTANCE' | 'READ_WEIGHT' | 'READ_HEIGHT' | 'READ_HEART_RATE' | 'READ_RESTING_HEART_RATE' | 'READ_ROUTE' | 'READ_MINDFULNESS' | 'READ_HRV' | 'READ_BLOOD_PRESSURE' | 'READ_BASAL_CALORIES' | 'READ_RESPIRATORY_RATE' | 'READ_OXYGEN_SATURATION' | 'READ_BLOOD_GLUCOSE' | 'READ_BODY_TEMPERATURE' | 'READ_BASAL_BODY_TEMPERATURE' | 'READ_BODY_FAT' | 'READ_FLOORS_CLIMBED' | 'READ_SLEEP' | 'READ_EXERCISE_TIME' | 'READ_BIOLOGICAL_SEX' | 'READ_BLOOD_TYPE' | 'READ_DATE_OF_BIRTH' | 'READ_FITZPATRICK_SKIN_TYPE' | 'READ_WHEELCHAIR_USE';
78
90
  export type LatestDataType = 'steps' | 'active-calories' | 'total-calories' | 'basal-calories' | 'distance' | 'weight' | 'height' | 'heart-rate' | 'resting-heart-rate' | 'respiratory-rate' | 'oxygen-saturation' | 'blood-glucose' | 'body-temperature' | 'basal-body-temperature' | 'body-fat' | 'flights-climbed' | 'exercise-time' | 'distance-cycling' | 'mindfulness' | 'sleep' | 'hrv' | 'blood-pressure';
79
91
  export interface PermissionsRequest {
80
92
  permissions: HealthPermission[];
@@ -142,3 +154,22 @@ export interface QueryLatestSampleResponse {
142
154
  unit: string;
143
155
  metadata?: Record<string, unknown>;
144
156
  }
157
+ export interface CharacteristicsResponse {
158
+ biologicalSex?: HealthBiologicalSex | null;
159
+ bloodType?: HealthBloodType | null;
160
+ dateOfBirth?: string | null;
161
+ fitzpatrickSkinType?: HealthFitzpatrickSkinType | null;
162
+ wheelchairUse?: HealthWheelchairUse | null;
163
+ /**
164
+ * Indicates whether the platform exposes these characteristics via the plugin (true on iOS, false on Android).
165
+ */
166
+ platformSupported?: boolean;
167
+ /**
168
+ * Optional platform-specific message; on Android we return a user-facing note explaining that values remain empty unless synced from iOS.
169
+ */
170
+ platformMessage?: string;
171
+ }
172
+ export type HealthBiologicalSex = 'female' | 'male' | 'other' | 'not_set' | 'unknown';
173
+ export type HealthBloodType = 'a-positive' | 'a-negative' | 'b-positive' | 'b-negative' | 'ab-positive' | 'ab-negative' | 'o-positive' | 'o-negative' | 'not_set' | 'unknown';
174
+ export type HealthFitzpatrickSkinType = 'type1' | 'type2' | 'type3' | 'type4' | 'type5' | 'type6' | 'not_set' | 'unknown';
175
+ export type HealthWheelchairUse = 'wheelchair_user' | 'not_wheelchair_user' | 'not_set' | 'unknown';
@@ -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 /**\n * Query latest sample for a specific data type\n * @param request\n */\n queryLatestSample(request: { dataType: LatestDataType }): Promise<QueryLatestSampleResponse>;\n\n /**\n * Query latest weight sample\n */\n queryWeight(): Promise<QueryLatestSampleResponse>;\n\n /**\n * Query latest height sample\n */\n queryHeight(): Promise<QueryLatestSampleResponse>;\n\n /**\n * Query latest heart rate sample\n */\n queryHeartRate(): Promise<QueryLatestSampleResponse>;\n\n /**\n * Query latest steps sample\n */\n querySteps(): Promise<QueryLatestSampleResponse>;\n}\n\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_RESTING_HEART_RATE'\n | 'READ_ROUTE'\n | 'READ_MINDFULNESS'\n | 'READ_HRV'\n | 'READ_BLOOD_PRESSURE'\n | 'READ_BASAL_CALORIES'\n | 'READ_RESPIRATORY_RATE'\n | 'READ_OXYGEN_SATURATION'\n | 'READ_BLOOD_GLUCOSE'\n | 'READ_BODY_TEMPERATURE'\n | 'READ_BASAL_BODY_TEMPERATURE'\n | 'READ_BODY_FAT'\n | 'READ_FLOORS_CLIMBED'\n | 'READ_SLEEP'\n | 'READ_EXERCISE_TIME';\n\nexport type LatestDataType =\n | 'steps'\n | 'active-calories'\n | 'total-calories'\n | 'basal-calories'\n | 'distance'\n | 'weight'\n | 'height'\n | 'heart-rate'\n | 'resting-heart-rate'\n | 'respiratory-rate'\n | 'oxygen-saturation'\n | 'blood-glucose'\n | 'body-temperature'\n | 'basal-body-temperature'\n | 'body-fat'\n | 'flights-climbed'\n | 'exercise-time'\n | 'distance-cycling'\n | 'mindfulness'\n | 'sleep'\n | 'hrv'\n | 'blood-pressure';\n\nexport interface PermissionsRequest {\n permissions: HealthPermission[];\n}\n\nexport interface PermissionResponse {\n permissions: Record<HealthPermission, boolean>;\n}\n\nexport interface QueryWorkoutRequest {\n startDate: string;\n endDate: string;\n includeHeartRate: boolean;\n includeRoute: boolean;\n includeSteps: boolean;\n}\n\nexport interface HeartRateSample {\n timestamp: string;\n bpm: number;\n}\n\nexport interface RouteSample {\n timestamp: string;\n lat: number;\n lng: number;\n alt?: number;\n}\n\nexport interface QueryWorkoutResponse {\n workouts: Workout[];\n}\n\nexport interface Workout {\n startDate: string;\n endDate: string;\n workoutType: string;\n sourceName: string;\n id?: string;\n duration: number;\n distance?: number;\n steps?: number;\n calories: number;\n sourceBundleId: string;\n route?: RouteSample[];\n heartRate?: HeartRateSample[];\n}\n\nexport interface QueryAggregatedRequest {\n startDate: string;\n endDate: string;\n dataType:\n | 'steps'\n | 'active-calories'\n | 'total-calories'\n | 'basal-calories'\n | 'distance'\n | 'weight'\n | 'height'\n | 'heart-rate'\n | 'resting-heart-rate'\n | 'respiratory-rate'\n | 'oxygen-saturation'\n | 'blood-glucose'\n | 'body-temperature'\n | 'basal-body-temperature'\n | 'body-fat'\n | 'flights-climbed'\n | 'exercise-time'\n | 'distance-cycling'\n | 'sleep'\n | 'mindfulness'\n | 'hrv'\n | 'blood-pressure';\n bucket: string;\n}\n\nexport interface QueryAggregatedResponse {\n aggregatedData: AggregatedSample[];\n}\n\nexport interface AggregatedSample {\n startDate: string;\n endDate: string;\n value: number;\n systolic?: number;\n diastolic?: number;\n unit?: string;\n}\n\nexport interface QueryLatestSampleResponse {\n value?: number;\n systolic?: number;\n diastolic?: number;\n timestamp: number;\n endTimestamp?: number;\n unit: string;\n metadata?: Record<string, unknown>;\n}\n"]}
1
+ {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["export interface HealthPlugin {\n /**\n * Checks if health API is available.\n * Android: If false is returned, the Google Health Connect app is probably not installed.\n * See showHealthConnectInPlayStore()\n *\n */\n isHealthAvailable(): Promise<{ available: boolean }>;\n\n /**\n * Android only: Returns for each given permission, if it was granted by the underlying health API\n * @param permissions permissions to query\n */\n checkHealthPermissions(permissions: PermissionsRequest): Promise<PermissionResponse>;\n\n /**\n * Requests the permissions from the user.\n *\n * Android: Apps can ask only a few times for permissions, after that the user has to grant them manually in\n * the Health Connect app. See openHealthConnectSettings()\n *\n * iOS: If the permissions are already granted or denied, this method will just return without asking the user. In iOS\n * we can't really detect if a user granted or denied a permission. The return value reflects the assumption that all\n * permissions were granted.\n *\n * @param permissions permissions to request\n */\n requestHealthPermissions(permissions: PermissionsRequest): Promise<PermissionResponse>;\n\n /**\n * Opens the apps settings, which is kind of wrong, because health permissions are configured under:\n * Settings > Apps > (Apple) Health > Access and Devices > [app-name]\n * But we can't go there directly.\n */\n openAppleHealthSettings(): Promise<void>;\n\n /**\n * Opens the Google Health Connect app\n */\n openHealthConnectSettings(): Promise<void>;\n\n /**\n * Opens the Google Health Connect app in PlayStore\n */\n showHealthConnectInPlayStore(): Promise<void>;\n\n /**\n * iOS only: Reads user characteristics such as biological sex, blood type, date of birth, Fitzpatrick skin type, and wheelchair use.\n * Values are null when unavailable or permission was not granted. Android does not expose these characteristics; it returns `platformSupported: false` and a `platformMessage` for UI hints without emitting null values.\n */\n getCharacteristics(): Promise<CharacteristicsResponse>;\n\n /**\n * Query aggregated data\n * - Blood-pressure aggregates return the systolic average in `value` plus `systolic`, `diastolic`, and `unit`.\n * - `total-calories` is derived as active + basal energy on both iOS and Android for latest samples, aggregated queries, and workouts. We fall back to the platform's total‑calories metric (or active calories) when basal data isn't available or permission is missing. Request both `READ_ACTIVE_CALORIES` and `READ_BASAL_CALORIES` for full totals.\n * - Weight/height aggregation returns the latest sample per day (no averaging).\n * - Android aggregation currently supports daily buckets; unsupported buckets will be rejected.\n * - Android `distance-cycling` aggregates distance recorded during biking exercise sessions (requires distance + workouts permissions).\n * - Daily `bucket: \"day\"` queries use calendar-day boundaries in the device time zone (start-of-day through the next start-of-day) instead of a trailing 24-hour window. For “today,” send `startDate` at today’s start-of-day and `endDate` at now or tomorrow’s start-of-day.\n * @param request\n */\n queryAggregated(request: QueryAggregatedRequest): Promise<QueryAggregatedResponse>;\n\n /**\n * Query workouts\n * @param request\n */\n queryWorkouts(request: QueryWorkoutRequest): Promise<QueryWorkoutResponse>;\n\n /**\n * Query latest sample for a specific data type\n * - Latest sleep sample returns the most recent complete sleep session (asleep states only) from the last ~36 hours; if a longer overnight session exists, shorter naps are ignored.\n * @param request\n */\n queryLatestSample(request: { dataType: LatestDataType }): Promise<QueryLatestSampleResponse>;\n\n /**\n * Query latest weight sample\n */\n queryWeight(): Promise<QueryLatestSampleResponse>;\n\n /**\n * Query latest height sample\n */\n queryHeight(): Promise<QueryLatestSampleResponse>;\n\n /**\n * Query latest heart rate sample\n */\n queryHeartRate(): Promise<QueryLatestSampleResponse>;\n\n /**\n * Query latest steps sample\n */\n querySteps(): Promise<QueryLatestSampleResponse>;\n}\n\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_RESTING_HEART_RATE'\n | 'READ_ROUTE'\n | 'READ_MINDFULNESS'\n | 'READ_HRV'\n | 'READ_BLOOD_PRESSURE'\n | 'READ_BASAL_CALORIES'\n | 'READ_RESPIRATORY_RATE'\n | 'READ_OXYGEN_SATURATION'\n | 'READ_BLOOD_GLUCOSE'\n | 'READ_BODY_TEMPERATURE'\n | 'READ_BASAL_BODY_TEMPERATURE'\n | 'READ_BODY_FAT'\n | 'READ_FLOORS_CLIMBED'\n | 'READ_SLEEP'\n | 'READ_EXERCISE_TIME'\n | 'READ_BIOLOGICAL_SEX'\n | 'READ_BLOOD_TYPE'\n | 'READ_DATE_OF_BIRTH'\n | 'READ_FITZPATRICK_SKIN_TYPE'\n | 'READ_WHEELCHAIR_USE';\n\nexport type LatestDataType =\n | 'steps'\n | 'active-calories'\n | 'total-calories'\n | 'basal-calories'\n | 'distance'\n | 'weight'\n | 'height'\n | 'heart-rate'\n | 'resting-heart-rate'\n | 'respiratory-rate'\n | 'oxygen-saturation'\n | 'blood-glucose'\n | 'body-temperature'\n | 'basal-body-temperature'\n | 'body-fat'\n | 'flights-climbed'\n | 'exercise-time'\n | 'distance-cycling'\n | 'mindfulness'\n | 'sleep'\n | 'hrv'\n | 'blood-pressure';\n\nexport interface PermissionsRequest {\n permissions: HealthPermission[];\n}\n\nexport interface PermissionResponse {\n permissions: Record<HealthPermission, boolean>;\n}\n\nexport interface QueryWorkoutRequest {\n startDate: string;\n endDate: string;\n includeHeartRate: boolean;\n includeRoute: boolean;\n includeSteps: boolean;\n}\n\nexport interface HeartRateSample {\n timestamp: string;\n bpm: number;\n}\n\nexport interface RouteSample {\n timestamp: string;\n lat: number;\n lng: number;\n alt?: number;\n}\n\nexport interface QueryWorkoutResponse {\n workouts: Workout[];\n}\n\nexport interface Workout {\n startDate: string;\n endDate: string;\n workoutType: string;\n sourceName: string;\n id?: string;\n duration: number;\n distance?: number;\n steps?: number;\n calories: number;\n sourceBundleId: string;\n route?: RouteSample[];\n heartRate?: HeartRateSample[];\n}\n\nexport interface QueryAggregatedRequest {\n startDate: string;\n endDate: string;\n dataType:\n | 'steps'\n | 'active-calories'\n | 'total-calories'\n | 'basal-calories'\n | 'distance'\n | 'weight'\n | 'height'\n | 'heart-rate'\n | 'resting-heart-rate'\n | 'respiratory-rate'\n | 'oxygen-saturation'\n | 'blood-glucose'\n | 'body-temperature'\n | 'basal-body-temperature'\n | 'body-fat'\n | 'flights-climbed'\n | 'exercise-time'\n | 'distance-cycling'\n | 'sleep'\n | 'mindfulness'\n | 'hrv'\n | 'blood-pressure';\n bucket: string;\n}\n\nexport interface QueryAggregatedResponse {\n aggregatedData: AggregatedSample[];\n}\n\nexport interface AggregatedSample {\n startDate: string;\n endDate: string;\n value: number;\n systolic?: number;\n diastolic?: number;\n unit?: string;\n}\n\nexport interface QueryLatestSampleResponse {\n value?: number;\n systolic?: number;\n diastolic?: number;\n timestamp: number;\n endTimestamp?: number;\n unit: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface CharacteristicsResponse {\n biologicalSex?: HealthBiologicalSex | null;\n bloodType?: HealthBloodType | null;\n dateOfBirth?: string | null;\n fitzpatrickSkinType?: HealthFitzpatrickSkinType | null;\n wheelchairUse?: HealthWheelchairUse | null;\n /**\n * Indicates whether the platform exposes these characteristics via the plugin (true on iOS, false on Android).\n */\n platformSupported?: boolean;\n /**\n * Optional platform-specific message; on Android we return a user-facing note explaining that values remain empty unless synced from iOS.\n */\n platformMessage?: string;\n}\n\nexport type HealthBiologicalSex = 'female' | 'male' | 'other' | 'not_set' | 'unknown';\n\nexport type HealthBloodType =\n | 'a-positive'\n | 'a-negative'\n | 'b-positive'\n | 'b-negative'\n | 'ab-positive'\n | 'ab-negative'\n | 'o-positive'\n | 'o-negative'\n | 'not_set'\n | 'unknown';\n\nexport type HealthFitzpatrickSkinType = 'type1' | 'type2' | 'type3' | 'type4' | 'type5' | 'type6' | 'not_set' | 'unknown';\n\nexport type HealthWheelchairUse = 'wheelchair_user' | 'not_wheelchair_user' | 'not_set' | 'unknown';\n"]}
@@ -18,7 +18,8 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
18
18
  CAPPluginMethod(name: "openAppleHealthSettings", returnType: CAPPluginReturnPromise),
19
19
  CAPPluginMethod(name: "queryAggregated", returnType: CAPPluginReturnPromise),
20
20
  CAPPluginMethod(name: "queryWorkouts", returnType: CAPPluginReturnPromise),
21
- CAPPluginMethod(name: "queryLatestSample", returnType: CAPPluginReturnPromise)
21
+ CAPPluginMethod(name: "queryLatestSample", returnType: CAPPluginReturnPromise),
22
+ CAPPluginMethod(name: "getCharacteristics", returnType: CAPPluginReturnPromise)
22
23
  ]
23
24
 
24
25
  let healthStore = HKHealthStore()
@@ -96,6 +97,47 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
96
97
  }
97
98
  }
98
99
 
100
+ @objc func getCharacteristics(_ call: CAPPluginCall) {
101
+ var result: [String: Any] = [:]
102
+
103
+ func setValue(_ key: String, _ value: String?) {
104
+ result[key] = value ?? NSNull()
105
+ }
106
+
107
+ if let biologicalSexObject = try? healthStore.biologicalSex() {
108
+ setValue("biologicalSex", mapBiologicalSex(biologicalSexObject.biologicalSex))
109
+ } else {
110
+ setValue("biologicalSex", nil)
111
+ }
112
+
113
+ if let bloodTypeObject = try? healthStore.bloodType() {
114
+ setValue("bloodType", mapBloodType(bloodTypeObject.bloodType))
115
+ } else {
116
+ setValue("bloodType", nil)
117
+ }
118
+
119
+ if let dateComponents = try? healthStore.dateOfBirthComponents() {
120
+ setValue("dateOfBirth", isoBirthDateString(from: dateComponents))
121
+ } else {
122
+ setValue("dateOfBirth", nil)
123
+ }
124
+
125
+ if let fitzpatrickObject = try? healthStore.fitzpatrickSkinType() {
126
+ setValue("fitzpatrickSkinType", mapFitzpatrickSkinType(fitzpatrickObject.skinType))
127
+ } else {
128
+ setValue("fitzpatrickSkinType", nil)
129
+ }
130
+
131
+ if let wheelchairUseObject = try? healthStore.wheelchairUse() {
132
+ setValue("wheelchairUse", mapWheelchairUse(wheelchairUseObject.wheelchairUse))
133
+ } else {
134
+ setValue("wheelchairUse", nil)
135
+ }
136
+
137
+ result["platformSupported"] = true
138
+ call.resolve(result)
139
+ }
140
+
99
141
  @objc func queryLatestSample(_ call: CAPPluginCall) {
100
142
  guard let dataTypeString = call.getString("dataType") else {
101
143
  call.reject("Missing data type")
@@ -473,6 +515,16 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
473
515
  return [HKObjectType.categoryType(forIdentifier: .sleepAnalysis)!].compactMap { $0 }
474
516
  case "READ_EXERCISE_TIME":
475
517
  return [HKObjectType.quantityType(forIdentifier: .appleExerciseTime)].compactMap { $0 }
518
+ case "READ_BIOLOGICAL_SEX":
519
+ return [HKObjectType.characteristicType(forIdentifier: .biologicalSex)].compactMap { $0 }
520
+ case "READ_BLOOD_TYPE":
521
+ return [HKObjectType.characteristicType(forIdentifier: .bloodType)].compactMap { $0 }
522
+ case "READ_DATE_OF_BIRTH":
523
+ return [HKObjectType.characteristicType(forIdentifier: .dateOfBirth)].compactMap { $0 }
524
+ case "READ_FITZPATRICK_SKIN_TYPE":
525
+ return [HKObjectType.characteristicType(forIdentifier: .fitzpatrickSkinType)].compactMap { $0 }
526
+ case "READ_WHEELCHAIR_USE":
527
+ return [HKObjectType.characteristicType(forIdentifier: .wheelchairUse)].compactMap { $0 }
476
528
  // Add common alternative permission names
477
529
  case "steps":
478
530
  return [HKObjectType.quantityType(forIdentifier: .stepCount)].compactMap{$0}
@@ -534,6 +586,94 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
534
586
  return []
535
587
  }
536
588
  }
589
+
590
+ private func mapBiologicalSex(_ biologicalSex: HKBiologicalSex) -> String {
591
+ switch biologicalSex {
592
+ case .female:
593
+ return "female"
594
+ case .male:
595
+ return "male"
596
+ case .other:
597
+ return "other"
598
+ case .notSet:
599
+ return "not_set"
600
+ @unknown default:
601
+ return "unknown"
602
+ }
603
+ }
604
+
605
+ private func mapBloodType(_ bloodType: HKBloodType) -> String {
606
+ switch bloodType {
607
+ case .aPositive:
608
+ return "a-positive"
609
+ case .aNegative:
610
+ return "a-negative"
611
+ case .bPositive:
612
+ return "b-positive"
613
+ case .bNegative:
614
+ return "b-negative"
615
+ case .abPositive:
616
+ return "ab-positive"
617
+ case .abNegative:
618
+ return "ab-negative"
619
+ case .oPositive:
620
+ return "o-positive"
621
+ case .oNegative:
622
+ return "o-negative"
623
+ case .notSet:
624
+ return "not_set"
625
+ @unknown default:
626
+ return "unknown"
627
+ }
628
+ }
629
+
630
+ private func mapFitzpatrickSkinType(_ skinType: HKFitzpatrickSkinType) -> String {
631
+ switch skinType {
632
+ case .I:
633
+ return "type1"
634
+ case .II:
635
+ return "type2"
636
+ case .III:
637
+ return "type3"
638
+ case .IV:
639
+ return "type4"
640
+ case .V:
641
+ return "type5"
642
+ case .VI:
643
+ return "type6"
644
+ case .notSet:
645
+ return "not_set"
646
+ @unknown default:
647
+ return "unknown"
648
+ }
649
+ }
650
+
651
+ private func mapWheelchairUse(_ wheelchairUse: HKWheelchairUse) -> String {
652
+ switch wheelchairUse {
653
+ case .yes:
654
+ return "wheelchair_user"
655
+ case .no:
656
+ return "not_wheelchair_user"
657
+ case .notSet:
658
+ return "not_set"
659
+ @unknown default:
660
+ return "unknown"
661
+ }
662
+ }
663
+
664
+ private func isoBirthDateString(from components: DateComponents) -> String? {
665
+ var calendar = Calendar(identifier: .gregorian)
666
+ calendar.timeZone = TimeZone(secondsFromGMT: 0) ?? TimeZone.current
667
+ guard let date = calendar.date(from: components) else {
668
+ return nil
669
+ }
670
+ let formatter = DateFormatter()
671
+ formatter.calendar = calendar
672
+ formatter.locale = Locale(identifier: "en_US_POSIX")
673
+ formatter.timeZone = calendar.timeZone
674
+ formatter.dateFormat = "yyyy-MM-dd"
675
+ return formatter.string(from: date)
676
+ }
537
677
 
538
678
  func aggregateTypeToHKQuantityType(_ dataType: String) -> HKQuantityType? {
539
679
  switch dataType {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flomentumsolutions/capacitor-health-extended",
3
- "version": "0.4.5",
3
+ "version": "0.6.1",
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",