@flomentumsolutions/capacitor-health-extended 0.7.6 → 0.8.0

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
@@ -227,6 +227,38 @@ class PermissionsRationaleActivity : AppCompatActivity() {
227
227
 
228
228
  This setup ensures your WebView will load HTTPS content securely and complies with Android's default network security policy.
229
229
 
230
+ ## iOS read-permission UX hint
231
+
232
+ On iOS, `checkHealthPermissions` is optimistic for read sample permissions because HealthKit does not distinguish
233
+ denied access from empty data. If a query returns no results, prompt the user to verify Apple Health settings.
234
+
235
+ ```typescript
236
+ import { Health, type HealthPermission } from '@flomentumsolutions/capacitor-health-extended';
237
+
238
+ const NO_DATA_HINT =
239
+ 'No data found. If you believe this is an error, please verify your settings in Apple Health.';
240
+
241
+ export async function queryWithHealthEmptyHint<T>(
242
+ permission: HealthPermission,
243
+ query: () => Promise<T>,
244
+ isEmpty: (result: T) => boolean,
245
+ showHint: (message: string) => void
246
+ ): Promise<T | null> {
247
+ const { permissions } = await Health.checkHealthPermissions({ permissions: [permission] });
248
+ if (!permissions[permission]) {
249
+ return null;
250
+ }
251
+
252
+ const result = await query();
253
+
254
+ if (isEmpty(result)) {
255
+ showHint(NO_DATA_HINT);
256
+ }
257
+
258
+ return result;
259
+ }
260
+ ```
261
+
230
262
  ## API
231
263
  ```
232
264
  <docgen-index>
@@ -237,7 +269,7 @@ This setup ensures your WebView will load HTTPS content securely and complies wi
237
269
  * [`openAppleHealthSettings()`](#openapplehealthsettings)
238
270
  * [`openHealthConnectSettings()`](#openhealthconnectsettings)
239
271
  * [`showHealthConnectInPlayStore()`](#showhealthconnectinplaystore)
240
- * [`getCharacteristics()`](#getcharacteristics)
272
+ * [`getCharacteristics(...)`](#getcharacteristics)
241
273
  * [`queryAggregated(...)`](#queryaggregated)
242
274
  * [`queryWorkouts(...)`](#queryworkouts)
243
275
  * [`queryLatestSample(...)`](#querylatestsample)
@@ -276,7 +308,12 @@ See showHealthConnectInPlayStore()
276
308
  checkHealthPermissions(permissions: PermissionsRequest) => Promise<PermissionResponse>
277
309
  ```
278
310
 
279
- Android only: Returns for each given permission, if it was granted by the underlying health API
311
+ Returns whether each permission is granted.
312
+ Android: Uses Health Connect grant state.
313
+ iOS: Write permissions are strict. Read permissions for sample types are optimistic because HealthKit does not
314
+ distinguish denied vs no data; this returns false only when the read permission is not determined. For
315
+ characteristics, this probes access and returns false when denied.
316
+ UX tip: If this returns true but a query yields no results, show a hint to check Apple Health settings.
280
317
 
281
318
  | Param | Type | Description |
282
319
  | ----------------- | ----------------------------------------------------------------- | -------------------- |
@@ -331,6 +368,7 @@ openHealthConnectSettings() => Promise<void>
331
368
  ```
332
369
 
333
370
  Opens the Google Health Connect app
371
+ iOS: Aliases openAppleHealthSettings().
334
372
 
335
373
  --------------------
336
374
 
@@ -342,19 +380,22 @@ showHealthConnectInPlayStore() => Promise<void>
342
380
  ```
343
381
 
344
382
  Opens the Google Health Connect app in PlayStore
383
+ iOS: Resolves without action.
345
384
 
346
385
  --------------------
347
386
 
348
387
 
349
- ### getCharacteristics()
388
+ ### getCharacteristics(...)
350
389
 
351
390
  ```typescript
352
- getCharacteristics(request?: CharacteristicsRequest) => Promise<CharacteristicsResponse>
391
+ getCharacteristics(request?: CharacteristicsRequest | undefined) => Promise<CharacteristicsResponse>
353
392
  ```
354
393
 
355
- iOS only: Reads user characteristics such as biological sex, blood type, date of birth, Fitzpatrick skin type, and wheelchair use. Pass `fields` to request a single characteristic (e.g., date of birth) and keep permissions narrowly scoped; defaults to all when omitted.
394
+ iOS only: Reads user characteristics such as biological sex, blood type, date of birth, Fitzpatrick skin type, and wheelchair use.
356
395
  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.
357
396
 
397
+ Passing `fields` lets you request only specific characteristics (e.g., date of birth) to keep permissions scoped narrowly. Defaults to all characteristics when omitted.
398
+
358
399
  | Param | Type |
359
400
  | ------------- | ------------------------------------------------------------------------- |
360
401
  | **`request`** | <code><a href="#characteristicsrequest">CharacteristicsRequest</a></code> |
@@ -430,6 +471,7 @@ queryWeight() => Promise<QueryLatestSampleResponse>
430
471
  ```
431
472
 
432
473
  Query latest weight sample
474
+ Convenience wrapper around queryLatestSample({ dataType: 'weight' }).
433
475
 
434
476
  **Returns:** <code>Promise&lt;<a href="#querylatestsampleresponse">QueryLatestSampleResponse</a>&gt;</code>
435
477
 
@@ -443,6 +485,7 @@ queryHeight() => Promise<QueryLatestSampleResponse>
443
485
  ```
444
486
 
445
487
  Query latest height sample
488
+ Convenience wrapper around queryLatestSample({ dataType: 'height' }).
446
489
 
447
490
  **Returns:** <code>Promise&lt;<a href="#querylatestsampleresponse">QueryLatestSampleResponse</a>&gt;</code>
448
491
 
@@ -456,6 +499,7 @@ queryHeartRate() => Promise<QueryLatestSampleResponse>
456
499
  ```
457
500
 
458
501
  Query latest heart rate sample
502
+ Convenience wrapper around queryLatestSample({ dataType: 'heart-rate' }).
459
503
 
460
504
  **Returns:** <code>Promise&lt;<a href="#querylatestsampleresponse">QueryLatestSampleResponse</a>&gt;</code>
461
505
 
@@ -469,6 +513,7 @@ querySteps() => Promise<QueryLatestSampleResponse>
469
513
  ```
470
514
 
471
515
  Query latest steps sample
516
+ Convenience wrapper around queryLatestSample({ dataType: 'steps' }).
472
517
 
473
518
  **Returns:** <code>Promise&lt;<a href="#querylatestsampleresponse">QueryLatestSampleResponse</a>&gt;</code>
474
519
 
@@ -529,13 +574,6 @@ Save user-provided body metrics to the health platform.
529
574
  | **`permissions`** | <code>HealthPermission[]</code> |
530
575
 
531
576
 
532
- #### CharacteristicsRequest
533
-
534
- | Prop | Type | Description |
535
- | ------------ | -------------------------------------------------------------------- | ---------------------------------------------------------- |
536
- | **`fields`** | <code><a href="#characteristicfield">CharacteristicField</a>[]</code> | Characteristics to query; defaults to all when unspecified |
537
-
538
-
539
577
  #### CharacteristicsResponse
540
578
 
541
579
  | Prop | Type | Description |
@@ -549,6 +587,13 @@ Save user-provided body metrics to the health platform.
549
587
  | **`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. |
550
588
 
551
589
 
590
+ #### CharacteristicsRequest
591
+
592
+ | Prop | Type | Description |
593
+ | ------------ | ---------------------------------- | ----------------------------------------------------------------------- |
594
+ | **`fields`** | <code>CharacteristicField[]</code> | Characteristics to query. Defaults to all characteristics when omitted. |
595
+
596
+
552
597
  #### QueryAggregatedResponse
553
598
 
554
599
  | Prop | Type |
@@ -695,11 +740,6 @@ Construct a type with a set of properties K of type T
695
740
  <code>{
696
741
  [P in K]: T;
697
742
  }</code>
698
743
 
699
744
 
700
- #### CharacteristicField
701
-
702
- <code>'biologicalSex' | 'bloodType' | 'dateOfBirth' | 'fitzpatrickSkinType' | 'wheelchairUse'</code>
703
-
704
-
705
745
  #### HealthPermission
706
746
 
707
747
  <code>'READ_STEPS' | 'READ_WORKOUTS' | 'WRITE_WORKOUTS' | 'READ_ACTIVE_CALORIES' | 'WRITE_ACTIVE_CALORIES' | 'READ_TOTAL_CALORIES' | 'WRITE_TOTAL_CALORIES' | 'READ_DISTANCE' | 'WRITE_DISTANCE' | 'READ_WEIGHT' | 'WRITE_WEIGHT' | 'READ_HEIGHT' | 'WRITE_HEIGHT' | 'READ_HEART_RATE' | 'WRITE_HEART_RATE' | 'READ_RESTING_HEART_RATE' | 'WRITE_RESTING_HEART_RATE' | 'READ_ROUTE' | 'WRITE_ROUTE' | 'READ_MINDFULNESS' | 'READ_HRV' | 'READ_BLOOD_PRESSURE' | 'READ_BASAL_CALORIES' | 'READ_RESPIRATORY_RATE' | 'READ_OXYGEN_SATURATION' | 'READ_BLOOD_GLUCOSE' | 'READ_BODY_TEMPERATURE' | 'READ_BASAL_BODY_TEMPERATURE' | 'READ_BODY_FAT' | 'WRITE_BODY_FAT' | 'READ_FLOORS_CLIMBED' | 'READ_SLEEP' | 'READ_EXERCISE_TIME' | 'READ_BIOLOGICAL_SEX' | 'READ_BLOOD_TYPE' | 'READ_DATE_OF_BIRTH' | 'READ_FITZPATRICK_SKIN_TYPE' | 'READ_WHEELCHAIR_USE'</code>
@@ -725,6 +765,11 @@ Construct a type with a set of properties K of type T
725
765
  <code>'wheelchair_user' | 'not_wheelchair_user' | 'not_set' | 'unknown'</code>
726
766
 
727
767
 
768
+ #### CharacteristicField
769
+
770
+ <code>'biologicalSex' | 'bloodType' | 'dateOfBirth' | 'fitzpatrickSkinType' | 'wheelchairUse'</code>
771
+
772
+
728
773
  #### LatestDataType
729
774
 
730
775
  <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' | 'sleep-rem' | 'hrv' | 'blood-pressure'</code>
@@ -9,7 +9,12 @@ export interface HealthPlugin {
9
9
  available: boolean;
10
10
  }>;
11
11
  /**
12
- * Android only: Returns for each given permission, if it was granted by the underlying health API
12
+ * Returns whether each permission is granted.
13
+ * Android: Uses Health Connect grant state.
14
+ * iOS: Write permissions are strict. Read permissions for sample types are optimistic because HealthKit does not
15
+ * distinguish denied vs no data; this returns false only when the read permission is not determined. For
16
+ * characteristics, this probes access and returns false when denied.
17
+ * UX tip: If this returns true but a query yields no results, show a hint to check Apple Health settings.
13
18
  * @param permissions permissions to query
14
19
  */
15
20
  checkHealthPermissions(permissions: PermissionsRequest): Promise<PermissionResponse>;
@@ -34,10 +39,12 @@ export interface HealthPlugin {
34
39
  openAppleHealthSettings(): Promise<void>;
35
40
  /**
36
41
  * Opens the Google Health Connect app
42
+ * iOS: Aliases openAppleHealthSettings().
37
43
  */
38
44
  openHealthConnectSettings(): Promise<void>;
39
45
  /**
40
46
  * Opens the Google Health Connect app in PlayStore
47
+ * iOS: Resolves without action.
41
48
  */
42
49
  showHealthConnectInPlayStore(): Promise<void>;
43
50
  /**
@@ -74,18 +81,22 @@ export interface HealthPlugin {
74
81
  }): Promise<QueryLatestSampleResponse>;
75
82
  /**
76
83
  * Query latest weight sample
84
+ * Convenience wrapper around queryLatestSample({ dataType: 'weight' }).
77
85
  */
78
86
  queryWeight(): Promise<QueryLatestSampleResponse>;
79
87
  /**
80
88
  * Query latest height sample
89
+ * Convenience wrapper around queryLatestSample({ dataType: 'height' }).
81
90
  */
82
91
  queryHeight(): Promise<QueryLatestSampleResponse>;
83
92
  /**
84
93
  * Query latest heart rate sample
94
+ * Convenience wrapper around queryLatestSample({ dataType: 'heart-rate' }).
85
95
  */
86
96
  queryHeartRate(): Promise<QueryLatestSampleResponse>;
87
97
  /**
88
98
  * Query latest steps sample
99
+ * Convenience wrapper around queryLatestSample({ dataType: 'steps' }).
89
100
  */
90
101
  querySteps(): Promise<QueryLatestSampleResponse>;
91
102
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["export interface HealthPlugin {\n /**\n * Checks if health API is available.\n * Android: If false is returned, the Google Health Connect app is probably not installed.\n * See showHealthConnectInPlayStore()\n *\n */\n isHealthAvailable(): Promise<{ available: boolean }>;\n\n /**\n * Android only: Returns for each given permission, if it was granted by the underlying health API\n * @param permissions permissions to query\n */\n checkHealthPermissions(permissions: PermissionsRequest): Promise<PermissionResponse>;\n\n /**\n * Requests the permissions from the user.\n *\n * Android: Apps can ask only a few times for permissions, after that the user has to grant them manually in\n * the Health Connect app. See openHealthConnectSettings()\n *\n * iOS: If the permissions are already granted or denied, this method will just return without asking the user. In iOS\n * we can't really detect if a user granted or denied a permission. The return value reflects the assumption that all\n * permissions were granted.\n *\n * @param permissions permissions to request\n */\n requestHealthPermissions(permissions: PermissionsRequest): Promise<PermissionResponse>;\n\n /**\n * Opens the apps settings, which is kind of wrong, because health permissions are configured under:\n * Settings > Apps > (Apple) Health > Access and Devices > [app-name]\n * But we can't go there directly.\n */\n openAppleHealthSettings(): Promise<void>;\n\n /**\n * Opens the Google Health Connect app\n */\n openHealthConnectSettings(): Promise<void>;\n\n /**\n * Opens the Google Health Connect app in PlayStore\n */\n showHealthConnectInPlayStore(): Promise<void>;\n\n /**\n * iOS only: Reads user characteristics such as biological sex, blood type, date of birth, Fitzpatrick skin type, and wheelchair use.\n * Values are null when unavailable or permission was not granted. Android does not expose these characteristics; it returns `platformSupported: false` and a `platformMessage` for UI hints without emitting null values.\n *\n * Passing `fields` lets you request only specific characteristics (e.g., date of birth) to keep permissions scoped narrowly. Defaults to all characteristics when omitted.\n */\n getCharacteristics(request?: CharacteristicsRequest): Promise<CharacteristicsResponse>;\n\n /**\n * Query aggregated data\n * - Blood-pressure aggregates return the systolic average in `value` plus `systolic`, `diastolic`, and `unit`.\n * - `total-calories` is derived as active + basal energy on both iOS and Android for latest samples, aggregated queries, and workouts. We fall back to the platform's total‑calories metric (or active calories) when basal data isn't available or permission is missing. Request both `READ_ACTIVE_CALORIES` and `READ_BASAL_CALORIES` for full totals.\n * - Weight/height aggregation returns the latest sample per day (no averaging).\n * - Android aggregation currently supports daily buckets; unsupported buckets will be rejected.\n * - Android `distance-cycling` aggregates distance recorded during biking exercise sessions (requires distance + workouts permissions).\n * - Daily `bucket: \"day\"` queries use calendar-day boundaries in the device time zone (start-of-day through the next start-of-day) instead of a trailing 24-hour window. For “today,” send `startDate` at today’s start-of-day and `endDate` at now or tomorrow’s start-of-day.\n * @param request\n */\n queryAggregated(request: QueryAggregatedRequest): Promise<QueryAggregatedResponse>;\n\n /**\n * Query workouts\n * @param request\n */\n queryWorkouts(request: QueryWorkoutRequest): Promise<QueryWorkoutResponse>;\n\n /**\n * Query latest sample for a specific data type\n * - Latest sleep sample returns the most recent complete sleep session (asleep states only) from the last ~36 hours; if a longer overnight session exists, shorter naps are ignored.\n * - `sleep-rem` returns REM duration (minutes) for the latest sleep session; requires iOS 16+ sleep stages and Health Connect REM data on Android.\n * @param request\n */\n queryLatestSample(request: { dataType: LatestDataType }): Promise<QueryLatestSampleResponse>;\n\n /**\n * Query latest weight sample\n */\n queryWeight(): Promise<QueryLatestSampleResponse>;\n\n /**\n * Query latest height sample\n */\n queryHeight(): Promise<QueryLatestSampleResponse>;\n\n /**\n * Query latest heart rate sample\n */\n queryHeartRate(): Promise<QueryLatestSampleResponse>;\n\n /**\n * Query latest steps sample\n */\n querySteps(): Promise<QueryLatestSampleResponse>;\n\n /**\n * Create a workout session with optional totals and route/heart-rate samples.\n * - iOS stores an `HKWorkout` (activityType mapped from `activityType`) with total energy/distance and optional metadata/route/heart-rate samples.\n * - Android stores an `ExerciseSessionRecord` plus `ActiveCaloriesBurnedRecord`, `DistanceRecord`, and `HeartRateRecord` when provided. Routes are attached via `ExerciseRoute`.\n * - Requires matching WRITE_* permissions for the values you include (e.g., WRITE_WORKOUTS + WRITE_ACTIVE_CALORIES + WRITE_DISTANCE + WRITE_HEART_RATE + WRITE_ROUTE).\n */\n saveWorkout(request: SaveWorkoutRequest): Promise<SaveWorkoutResponse>;\n\n /**\n * Save user-provided body metrics to the health platform.\n */\n saveMetrics(request: SaveMetricsRequest): Promise<SaveMetricsResponse>;\n}\n\nexport declare type HealthPermission = 'READ_STEPS' | 'READ_WORKOUTS' | 'WRITE_WORKOUTS' | 'READ_ACTIVE_CALORIES' | 'WRITE_ACTIVE_CALORIES' | 'READ_TOTAL_CALORIES' | 'WRITE_TOTAL_CALORIES' | 'READ_DISTANCE' | 'WRITE_DISTANCE' | 'READ_WEIGHT' | 'WRITE_WEIGHT' | 'READ_HEIGHT' | 'WRITE_HEIGHT' | 'READ_HEART_RATE' | 'WRITE_HEART_RATE' | 'READ_RESTING_HEART_RATE' | 'WRITE_RESTING_HEART_RATE' | 'READ_ROUTE' | 'WRITE_ROUTE' | 'READ_MINDFULNESS' | 'READ_HRV' | 'READ_BLOOD_PRESSURE' | 'READ_BASAL_CALORIES' | 'READ_RESPIRATORY_RATE' | 'READ_OXYGEN_SATURATION' | 'READ_BLOOD_GLUCOSE' | 'READ_BODY_TEMPERATURE' | 'READ_BASAL_BODY_TEMPERATURE' | 'READ_BODY_FAT' | 'WRITE_BODY_FAT' | 'READ_FLOORS_CLIMBED' | 'READ_SLEEP' | 'READ_EXERCISE_TIME' | 'READ_BIOLOGICAL_SEX' | 'READ_BLOOD_TYPE' | 'READ_DATE_OF_BIRTH' | 'READ_FITZPATRICK_SKIN_TYPE' | 'READ_WHEELCHAIR_USE';\n\nexport type LatestDataType =\n | 'steps'\n | 'active-calories'\n | 'total-calories'\n | 'basal-calories'\n | 'distance'\n | 'weight'\n | 'height'\n | 'heart-rate'\n | 'resting-heart-rate'\n | 'respiratory-rate'\n | 'oxygen-saturation'\n | 'blood-glucose'\n | 'body-temperature'\n | 'basal-body-temperature'\n | 'body-fat'\n | 'flights-climbed'\n | 'exercise-time'\n | 'distance-cycling'\n | 'mindfulness'\n | 'sleep'\n | 'sleep-rem'\n | 'hrv'\n | 'blood-pressure';\n\nexport interface PermissionsRequest {\n permissions: HealthPermission[];\n}\n\nexport interface PermissionResponse {\n permissions: Record<HealthPermission, boolean>;\n}\n\nexport interface QueryWorkoutRequest {\n startDate: string;\n endDate: string;\n includeHeartRate: boolean;\n includeRoute: boolean;\n includeSteps: boolean;\n}\n\nexport interface HeartRateSample {\n timestamp: string;\n bpm: number;\n}\n\nexport interface RouteSample {\n timestamp: string;\n lat: number;\n lng: number;\n alt?: number;\n}\n\nexport interface QueryWorkoutResponse {\n workouts: Workout[];\n}\n\nexport type WorkoutActivityType =\n | 'rock-climbing'\n | 'climbing'\n | 'hiking'\n | 'running'\n | 'walking'\n | 'cycling'\n | 'biking'\n | 'strength-training'\n | 'yoga'\n | 'other';\n\nexport interface SaveWorkoutRequest {\n activityType: WorkoutActivityType;\n startDate: string;\n endDate: string;\n calories?: number;\n distance?: number;\n metadata?: Record<string, any>;\n route?: RouteSample[];\n heartRateSamples?: HeartRateSample[];\n}\n\nexport interface SaveWorkoutResponse {\n success: boolean;\n id?: string;\n}\n\nexport interface SaveMetricsRequest {\n weightKg?: number;\n heightCm?: number;\n bodyFatPercent?: number;\n restingHeartRate?: number;\n}\n\nexport interface SaveMetricsResponse {\n success: boolean;\n inserted?: number;\n}\n\nexport interface Workout {\n startDate: string;\n endDate: string;\n workoutType: string;\n sourceName: string;\n id?: string;\n duration: number;\n distance?: number;\n steps?: number;\n calories: number;\n sourceBundleId: string;\n route?: RouteSample[];\n heartRate?: HeartRateSample[];\n}\n\nexport interface QueryAggregatedRequest {\n startDate: string;\n endDate: string;\n dataType:\n | 'steps'\n | 'active-calories'\n | 'total-calories'\n | 'basal-calories'\n | 'distance'\n | 'weight'\n | 'height'\n | 'heart-rate'\n | 'resting-heart-rate'\n | 'respiratory-rate'\n | 'oxygen-saturation'\n | 'blood-glucose'\n | 'body-temperature'\n | 'basal-body-temperature'\n | 'body-fat'\n | 'flights-climbed'\n | 'exercise-time'\n | 'distance-cycling'\n | 'sleep'\n | 'mindfulness'\n | 'hrv'\n | 'blood-pressure';\n bucket: string;\n}\n\nexport interface QueryAggregatedResponse {\n aggregatedData: AggregatedSample[];\n}\n\nexport interface AggregatedSample {\n startDate: string;\n endDate: string;\n value: number;\n systolic?: number;\n diastolic?: number;\n unit?: string;\n}\n\nexport interface QueryLatestSampleResponse {\n value?: number;\n systolic?: number;\n diastolic?: number;\n timestamp: number;\n endTimestamp?: number;\n unit: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface CharacteristicsResponse {\n biologicalSex?: HealthBiologicalSex | null;\n bloodType?: HealthBloodType | null;\n dateOfBirth?: string | null;\n fitzpatrickSkinType?: HealthFitzpatrickSkinType | null;\n wheelchairUse?: HealthWheelchairUse | null;\n /**\n * Indicates whether the platform exposes these characteristics via the plugin (true on iOS, false on Android).\n */\n platformSupported?: boolean;\n /**\n * Optional platform-specific message; on Android we return a user-facing note explaining that values remain empty unless synced from iOS.\n */\n platformMessage?: string;\n}\n\nexport type CharacteristicField =\n | 'biologicalSex'\n | 'bloodType'\n | 'dateOfBirth'\n | 'fitzpatrickSkinType'\n | 'wheelchairUse';\n\nexport interface CharacteristicsRequest {\n /**\n * Characteristics to query. Defaults to all characteristics when omitted.\n */\n fields?: CharacteristicField[];\n}\n\nexport type HealthBiologicalSex = 'female' | 'male' | 'other' | 'not_set' | 'unknown';\n\nexport type HealthBloodType =\n | 'a-positive'\n | 'a-negative'\n | 'b-positive'\n | 'b-negative'\n | 'ab-positive'\n | 'ab-negative'\n | 'o-positive'\n | 'o-negative'\n | 'not_set'\n | 'unknown';\n\nexport type HealthFitzpatrickSkinType = 'type1' | 'type2' | 'type3' | 'type4' | 'type5' | 'type6' | 'not_set' | 'unknown';\n\nexport type HealthWheelchairUse = 'wheelchair_user' | 'not_wheelchair_user' | 'not_set' | 'unknown';\n"]}
1
+ {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["export interface HealthPlugin {\n /**\n * Checks if health API is available.\n * Android: If false is returned, the Google Health Connect app is probably not installed.\n * See showHealthConnectInPlayStore()\n *\n */\n isHealthAvailable(): Promise<{ available: boolean }>;\n\n /**\n * Returns whether each permission is granted.\n * Android: Uses Health Connect grant state.\n * iOS: Write permissions are strict. Read permissions for sample types are optimistic because HealthKit does not\n * distinguish denied vs no data; this returns false only when the read permission is not determined. For\n * characteristics, this probes access and returns false when denied.\n * UX tip: If this returns true but a query yields no results, show a hint to check Apple Health settings.\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 * iOS: Aliases openAppleHealthSettings().\n */\n openHealthConnectSettings(): Promise<void>;\n\n /**\n * Opens the Google Health Connect app in PlayStore\n * iOS: Resolves without action.\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 * Passing `fields` lets you request only specific characteristics (e.g., date of birth) to keep permissions scoped narrowly. Defaults to all characteristics when omitted.\n */\n getCharacteristics(request?: CharacteristicsRequest): Promise<CharacteristicsResponse>;\n\n /**\n * Query aggregated data\n * - Blood-pressure aggregates return the systolic average in `value` plus `systolic`, `diastolic`, and `unit`.\n * - `total-calories` is derived as active + basal energy on both iOS and Android for latest samples, aggregated queries, and workouts. We fall back to the platform's total‑calories metric (or active calories) when basal data isn't available or permission is missing. Request both `READ_ACTIVE_CALORIES` and `READ_BASAL_CALORIES` for full totals.\n * - Weight/height aggregation returns the latest sample per day (no averaging).\n * - Android aggregation currently supports daily buckets; unsupported buckets will be rejected.\n * - Android `distance-cycling` aggregates distance recorded during biking exercise sessions (requires distance + workouts permissions).\n * - Daily `bucket: \"day\"` queries use calendar-day boundaries in the device time zone (start-of-day through the next start-of-day) instead of a trailing 24-hour window. For “today,” send `startDate` at today’s start-of-day and `endDate` at now or tomorrow’s start-of-day.\n * @param request\n */\n queryAggregated(request: QueryAggregatedRequest): Promise<QueryAggregatedResponse>;\n\n /**\n * Query workouts\n * @param request\n */\n queryWorkouts(request: QueryWorkoutRequest): Promise<QueryWorkoutResponse>;\n\n /**\n * Query latest sample for a specific data type\n * - Latest sleep sample returns the most recent complete sleep session (asleep states only) from the last ~36 hours; if a longer overnight session exists, shorter naps are ignored.\n * - `sleep-rem` returns REM duration (minutes) for the latest sleep session; requires iOS 16+ sleep stages and Health Connect REM data on Android.\n * @param request\n */\n queryLatestSample(request: { dataType: LatestDataType }): Promise<QueryLatestSampleResponse>;\n\n /**\n * Query latest weight sample\n * Convenience wrapper around queryLatestSample({ dataType: 'weight' }).\n */\n queryWeight(): Promise<QueryLatestSampleResponse>;\n\n /**\n * Query latest height sample\n * Convenience wrapper around queryLatestSample({ dataType: 'height' }).\n */\n queryHeight(): Promise<QueryLatestSampleResponse>;\n\n /**\n * Query latest heart rate sample\n * Convenience wrapper around queryLatestSample({ dataType: 'heart-rate' }).\n */\n queryHeartRate(): Promise<QueryLatestSampleResponse>;\n\n /**\n * Query latest steps sample\n * Convenience wrapper around queryLatestSample({ dataType: 'steps' }).\n */\n querySteps(): Promise<QueryLatestSampleResponse>;\n\n /**\n * Create a workout session with optional totals and route/heart-rate samples.\n * - iOS stores an `HKWorkout` (activityType mapped from `activityType`) with total energy/distance and optional metadata/route/heart-rate samples.\n * - Android stores an `ExerciseSessionRecord` plus `ActiveCaloriesBurnedRecord`, `DistanceRecord`, and `HeartRateRecord` when provided. Routes are attached via `ExerciseRoute`.\n * - Requires matching WRITE_* permissions for the values you include (e.g., WRITE_WORKOUTS + WRITE_ACTIVE_CALORIES + WRITE_DISTANCE + WRITE_HEART_RATE + WRITE_ROUTE).\n */\n saveWorkout(request: SaveWorkoutRequest): Promise<SaveWorkoutResponse>;\n\n /**\n * Save user-provided body metrics to the health platform.\n */\n saveMetrics(request: SaveMetricsRequest): Promise<SaveMetricsResponse>;\n}\n\nexport declare type HealthPermission = 'READ_STEPS' | 'READ_WORKOUTS' | 'WRITE_WORKOUTS' | 'READ_ACTIVE_CALORIES' | 'WRITE_ACTIVE_CALORIES' | 'READ_TOTAL_CALORIES' | 'WRITE_TOTAL_CALORIES' | 'READ_DISTANCE' | 'WRITE_DISTANCE' | 'READ_WEIGHT' | 'WRITE_WEIGHT' | 'READ_HEIGHT' | 'WRITE_HEIGHT' | 'READ_HEART_RATE' | 'WRITE_HEART_RATE' | 'READ_RESTING_HEART_RATE' | 'WRITE_RESTING_HEART_RATE' | 'READ_ROUTE' | 'WRITE_ROUTE' | 'READ_MINDFULNESS' | 'READ_HRV' | 'READ_BLOOD_PRESSURE' | 'READ_BASAL_CALORIES' | 'READ_RESPIRATORY_RATE' | 'READ_OXYGEN_SATURATION' | 'READ_BLOOD_GLUCOSE' | 'READ_BODY_TEMPERATURE' | 'READ_BASAL_BODY_TEMPERATURE' | 'READ_BODY_FAT' | 'WRITE_BODY_FAT' | 'READ_FLOORS_CLIMBED' | 'READ_SLEEP' | 'READ_EXERCISE_TIME' | 'READ_BIOLOGICAL_SEX' | 'READ_BLOOD_TYPE' | 'READ_DATE_OF_BIRTH' | 'READ_FITZPATRICK_SKIN_TYPE' | 'READ_WHEELCHAIR_USE';\n\nexport type LatestDataType =\n | 'steps'\n | 'active-calories'\n | 'total-calories'\n | 'basal-calories'\n | 'distance'\n | 'weight'\n | 'height'\n | 'heart-rate'\n | 'resting-heart-rate'\n | 'respiratory-rate'\n | 'oxygen-saturation'\n | 'blood-glucose'\n | 'body-temperature'\n | 'basal-body-temperature'\n | 'body-fat'\n | 'flights-climbed'\n | 'exercise-time'\n | 'distance-cycling'\n | 'mindfulness'\n | 'sleep'\n | 'sleep-rem'\n | 'hrv'\n | 'blood-pressure';\n\nexport interface PermissionsRequest {\n permissions: HealthPermission[];\n}\n\nexport interface PermissionResponse {\n permissions: Record<HealthPermission, boolean>;\n}\n\nexport interface QueryWorkoutRequest {\n startDate: string;\n endDate: string;\n includeHeartRate: boolean;\n includeRoute: boolean;\n includeSteps: boolean;\n}\n\nexport interface HeartRateSample {\n timestamp: string;\n bpm: number;\n}\n\nexport interface RouteSample {\n timestamp: string;\n lat: number;\n lng: number;\n alt?: number;\n}\n\nexport interface QueryWorkoutResponse {\n workouts: Workout[];\n}\n\nexport type WorkoutActivityType =\n | 'rock-climbing'\n | 'climbing'\n | 'hiking'\n | 'running'\n | 'walking'\n | 'cycling'\n | 'biking'\n | 'strength-training'\n | 'yoga'\n | 'other';\n\nexport interface SaveWorkoutRequest {\n activityType: WorkoutActivityType;\n startDate: string;\n endDate: string;\n calories?: number;\n distance?: number;\n metadata?: Record<string, any>;\n route?: RouteSample[];\n heartRateSamples?: HeartRateSample[];\n}\n\nexport interface SaveWorkoutResponse {\n success: boolean;\n id?: string;\n}\n\nexport interface SaveMetricsRequest {\n weightKg?: number;\n heightCm?: number;\n bodyFatPercent?: number;\n restingHeartRate?: number;\n}\n\nexport interface SaveMetricsResponse {\n success: boolean;\n inserted?: number;\n}\n\nexport interface Workout {\n startDate: string;\n endDate: string;\n workoutType: string;\n sourceName: string;\n id?: string;\n duration: number;\n distance?: number;\n steps?: number;\n calories: number;\n sourceBundleId: string;\n route?: RouteSample[];\n heartRate?: HeartRateSample[];\n}\n\nexport interface QueryAggregatedRequest {\n startDate: string;\n endDate: string;\n dataType:\n | 'steps'\n | 'active-calories'\n | 'total-calories'\n | 'basal-calories'\n | 'distance'\n | 'weight'\n | 'height'\n | 'heart-rate'\n | 'resting-heart-rate'\n | 'respiratory-rate'\n | 'oxygen-saturation'\n | 'blood-glucose'\n | 'body-temperature'\n | 'basal-body-temperature'\n | 'body-fat'\n | 'flights-climbed'\n | 'exercise-time'\n | 'distance-cycling'\n | 'sleep'\n | 'mindfulness'\n | 'hrv'\n | 'blood-pressure';\n bucket: string;\n}\n\nexport interface QueryAggregatedResponse {\n aggregatedData: AggregatedSample[];\n}\n\nexport interface AggregatedSample {\n startDate: string;\n endDate: string;\n value: number;\n systolic?: number;\n diastolic?: number;\n unit?: string;\n}\n\nexport interface QueryLatestSampleResponse {\n value?: number;\n systolic?: number;\n diastolic?: number;\n timestamp: number;\n endTimestamp?: number;\n unit: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface CharacteristicsResponse {\n biologicalSex?: HealthBiologicalSex | null;\n bloodType?: HealthBloodType | null;\n dateOfBirth?: string | null;\n fitzpatrickSkinType?: HealthFitzpatrickSkinType | null;\n wheelchairUse?: HealthWheelchairUse | null;\n /**\n * Indicates whether the platform exposes these characteristics via the plugin (true on iOS, false on Android).\n */\n platformSupported?: boolean;\n /**\n * Optional platform-specific message; on Android we return a user-facing note explaining that values remain empty unless synced from iOS.\n */\n platformMessage?: string;\n}\n\nexport type CharacteristicField =\n | 'biologicalSex'\n | 'bloodType'\n | 'dateOfBirth'\n | 'fitzpatrickSkinType'\n | 'wheelchairUse';\n\nexport interface CharacteristicsRequest {\n /**\n * Characteristics to query. Defaults to all characteristics when omitted.\n */\n fields?: CharacteristicField[];\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"]}
@@ -17,9 +17,15 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
17
17
  CAPPluginMethod(name: "checkHealthPermissions", returnType: CAPPluginReturnPromise),
18
18
  CAPPluginMethod(name: "requestHealthPermissions", returnType: CAPPluginReturnPromise),
19
19
  CAPPluginMethod(name: "openAppleHealthSettings", returnType: CAPPluginReturnPromise),
20
+ CAPPluginMethod(name: "openHealthConnectSettings", returnType: CAPPluginReturnPromise),
21
+ CAPPluginMethod(name: "showHealthConnectInPlayStore", returnType: CAPPluginReturnPromise),
20
22
  CAPPluginMethod(name: "queryAggregated", returnType: CAPPluginReturnPromise),
21
23
  CAPPluginMethod(name: "queryWorkouts", returnType: CAPPluginReturnPromise),
22
24
  CAPPluginMethod(name: "queryLatestSample", returnType: CAPPluginReturnPromise),
25
+ CAPPluginMethod(name: "queryWeight", returnType: CAPPluginReturnPromise),
26
+ CAPPluginMethod(name: "queryHeight", returnType: CAPPluginReturnPromise),
27
+ CAPPluginMethod(name: "queryHeartRate", returnType: CAPPluginReturnPromise),
28
+ CAPPluginMethod(name: "querySteps", returnType: CAPPluginReturnPromise),
23
29
  CAPPluginMethod(name: "getCharacteristics", returnType: CAPPluginReturnPromise),
24
30
  CAPPluginMethod(name: "saveMetrics", returnType: CAPPluginReturnPromise),
25
31
  CAPPluginMethod(name: "saveWorkout", returnType: CAPPluginReturnPromise)
@@ -64,28 +70,94 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
64
70
  return
65
71
  }
66
72
 
67
- var result: [String: String] = [:]
73
+ var result: [String: Bool] = [:]
68
74
 
69
75
  for permission in permissions {
70
- let hkTypes = permissionToHKObjectType(permission) + permissionToHKSampleType(permission)
71
- for type in hkTypes {
72
- let status = healthStore.authorizationStatus(for: type)
76
+ // Resolve Mappings
77
+ let readTypes = permissionToHKObjectType(permission)
78
+ let shareTypes = permissionToHKSampleType(permission)
79
+
80
+ // Prevent invalid/typo permissions from defaulting to "true"
81
+ if readTypes.isEmpty && shareTypes.isEmpty {
82
+ print("⚡️ [HealthPlugin] Warning: Permission '\(permission)' is not mapped to any HealthKit types.")
83
+ result[permission] = false
84
+ continue
85
+ }
73
86
 
74
- switch status {
75
- case .notDetermined:
76
- result[permission] = "notDetermined"
77
- case .sharingDenied:
78
- result[permission] = "denied"
79
- case .sharingAuthorized:
80
- result[permission] = "authorized"
81
- @unknown default:
82
- result[permission] = "unknown"
87
+ // Determine Intent (Write vs Read)
88
+ // If the permission maps to any shareable types, treat it as a Write request.
89
+ let isWriteRequest = !shareTypes.isEmpty
90
+
91
+ var isGranted = true
92
+
93
+ if isWriteRequest {
94
+ // WRITE LOGIC: Strict
95
+ // Multi-type permissions must be All-or-Nothing for data integrity.
96
+ for type in shareTypes {
97
+ if healthStore.authorizationStatus(for: type) != .sharingAuthorized {
98
+ isGranted = false
99
+ break
100
+ }
101
+ }
102
+ } else {
103
+ // READ LOGIC: Hybrid (Strict Characteristic / Optimistic Sample)
104
+ for type in readTypes {
105
+ if let charType = type as? HKCharacteristicType {
106
+ // Characteristics: Strict Probe
107
+ // We strictly check these because they throw explicit errors on denial.
108
+ if !isCharacteristicAuthorized(charType) {
109
+ isGranted = false
110
+ break
111
+ }
112
+ } else {
113
+ // Sample Types: Optimistic Check
114
+ // Strategy:
115
+ // - If .notDetermined: We definitely need to ask. Return false.
116
+ // - If .sharingDenied: User explicitly denied OR it's a read-only masking.
117
+ // We MUST return 'true' here. If we return 'false', the app will loop
118
+ // requests forever because we cannot distinguish Denied from "No Data".
119
+ if healthStore.authorizationStatus(for: type) == .notDetermined {
120
+ isGranted = false
121
+ break
122
+ }
123
+ }
83
124
  }
84
125
  }
126
+
127
+ result[permission] = isGranted
85
128
  }
86
129
 
87
130
  call.resolve(["permissions": result])
88
131
  }
132
+
133
+ // Helper: Accurately checks Characteristic read access by attempting a fetch.
134
+ private func isCharacteristicAuthorized(_ type: HKCharacteristicType) -> Bool {
135
+ do {
136
+ switch type.identifier {
137
+ case HKCharacteristicTypeIdentifier.biologicalSex.rawValue:
138
+ _ = try healthStore.biologicalSex()
139
+ case HKCharacteristicTypeIdentifier.bloodType.rawValue:
140
+ _ = try healthStore.bloodType()
141
+ case HKCharacteristicTypeIdentifier.dateOfBirth.rawValue:
142
+ _ = try healthStore.dateOfBirthComponents()
143
+ case HKCharacteristicTypeIdentifier.fitzpatrickSkinType.rawValue:
144
+ _ = try healthStore.fitzpatrickSkinType()
145
+ case HKCharacteristicTypeIdentifier.wheelchairUse.rawValue:
146
+ _ = try healthStore.wheelchairUse()
147
+ default:
148
+ // Unknown characteristic identifiers should default to false to surface errors.
149
+ print("⚡️ [HealthPlugin] Warning: Unknown characteristic type probed: \(type.identifier)")
150
+ return false
151
+ }
152
+ return true
153
+ } catch let error as HKError {
154
+ // If the system explicitly says "Authorization Denied", we know for sure.
155
+ return error.code != .errorAuthorizationDenied
156
+ } catch {
157
+ // Other errors (device not supported, etc) implies we can't read it.
158
+ return false
159
+ }
160
+ }
89
161
 
90
162
  @objc func requestHealthPermissions(_ call: CAPPluginCall) {
91
163
  guard let permissions = call.getArray("permissions") as? [String] else {
@@ -264,7 +336,27 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
264
336
  call.reject("Missing data type")
265
337
  return
266
338
  }
267
-
339
+
340
+ queryLatestSample(for: dataTypeString, call: call)
341
+ }
342
+
343
+ @objc func queryWeight(_ call: CAPPluginCall) {
344
+ queryLatestSample(for: "weight", call: call)
345
+ }
346
+
347
+ @objc func queryHeight(_ call: CAPPluginCall) {
348
+ queryLatestSample(for: "height", call: call)
349
+ }
350
+
351
+ @objc func queryHeartRate(_ call: CAPPluginCall) {
352
+ queryLatestSample(for: "heart-rate", call: call)
353
+ }
354
+
355
+ @objc func querySteps(_ call: CAPPluginCall) {
356
+ queryLatestSample(for: "steps", call: call)
357
+ }
358
+
359
+ private func queryLatestSample(for dataTypeString: String, call: CAPPluginCall) {
268
360
  print("⚡️ [HealthPlugin] Querying latest sample for data type: \(dataTypeString)")
269
361
  // ---- Special handling for blood‑pressure correlation ----
270
362
  if dataTypeString == "blood-pressure" {
@@ -670,6 +762,14 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
670
762
  healthStore.execute(query)
671
763
  }
672
764
 
765
+ @objc func openHealthConnectSettings(_ call: CAPPluginCall) {
766
+ openAppleHealthSettings(call)
767
+ }
768
+
769
+ @objc func showHealthConnectInPlayStore(_ call: CAPPluginCall) {
770
+ call.resolve()
771
+ }
772
+
673
773
  @objc func openAppleHealthSettings(_ call: CAPPluginCall) {
674
774
  if let url = URL(string: UIApplication.openSettingsURLString) {
675
775
  DispatchQueue.main.async {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flomentumsolutions/capacitor-health-extended",
3
- "version": "0.7.6",
3
+ "version": "0.8.0",
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",