@flomentumsolutions/capacitor-health-extended 0.7.3 → 0.7.5
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 +18 -2
- package/android/src/main/java/com/flomentumsolutions/capacitor-health-extended/HealthPlugin.kt +5 -1
- package/dist/esm/definitions.d.ts +10 -1
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/HealthPluginPlugin/HealthPlugin.swift +110 -45
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -349,12 +349,16 @@ Opens the Google Health Connect app in PlayStore
|
|
|
349
349
|
### getCharacteristics()
|
|
350
350
|
|
|
351
351
|
```typescript
|
|
352
|
-
getCharacteristics() => Promise<CharacteristicsResponse>
|
|
352
|
+
getCharacteristics(request?: CharacteristicsRequest) => Promise<CharacteristicsResponse>
|
|
353
353
|
```
|
|
354
354
|
|
|
355
|
-
iOS only: Reads user characteristics such as biological sex, blood type, date of birth, Fitzpatrick skin type, and wheelchair use.
|
|
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.
|
|
356
356
|
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
357
|
|
|
358
|
+
| Param | Type |
|
|
359
|
+
| ------------- | ------------------------------------------------------------------------- |
|
|
360
|
+
| **`request`** | <code><a href="#characteristicsrequest">CharacteristicsRequest</a></code> |
|
|
361
|
+
|
|
358
362
|
**Returns:** <code>Promise<<a href="#characteristicsresponse">CharacteristicsResponse</a>></code>
|
|
359
363
|
|
|
360
364
|
--------------------
|
|
@@ -525,6 +529,13 @@ Save user-provided body metrics to the health platform.
|
|
|
525
529
|
| **`permissions`** | <code>HealthPermission[]</code> |
|
|
526
530
|
|
|
527
531
|
|
|
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
|
+
|
|
528
539
|
#### CharacteristicsResponse
|
|
529
540
|
|
|
530
541
|
| Prop | Type | Description |
|
|
@@ -684,6 +695,11 @@ Construct a type with a set of properties K of type T
|
|
|
684
695
|
<code>{
|
|
685
696
|
[P in K]: T;
|
|
686
697
|
}</code>
|
|
687
698
|
|
|
688
699
|
|
|
700
|
+
#### CharacteristicField
|
|
701
|
+
|
|
702
|
+
<code>'biologicalSex' | 'bloodType' | 'dateOfBirth' | 'fitzpatrickSkinType' | 'wheelchairUse'</code>
|
|
703
|
+
|
|
704
|
+
|
|
689
705
|
#### HealthPermission
|
|
690
706
|
|
|
691
707
|
<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>
|
package/android/src/main/java/com/flomentumsolutions/capacitor-health-extended/HealthPlugin.kt
CHANGED
|
@@ -350,11 +350,15 @@ class HealthPlugin : Plugin() {
|
|
|
350
350
|
|
|
351
351
|
@PluginMethod
|
|
352
352
|
fun getCharacteristics(call: PluginCall) {
|
|
353
|
+
val requestedFields = call.getArray("fields")
|
|
354
|
+
?.toList<String>()
|
|
355
|
+
?.filter { it.isNotBlank() }
|
|
356
|
+
?: emptyList()
|
|
353
357
|
val result = JSObject()
|
|
354
358
|
result.put("platformSupported", false)
|
|
355
359
|
result.put(
|
|
356
360
|
"platformMessage",
|
|
357
|
-
"Health Connect does not expose characteristics; this section stays empty unless synced from an iOS device."
|
|
361
|
+
"Health Connect does not expose characteristics; this section stays empty unless synced from an iOS device.${if (requestedFields.isEmpty()) "" else " Requested fields: ${requestedFields.joinToString(\", \")}."}"
|
|
358
362
|
)
|
|
359
363
|
call.resolve(result)
|
|
360
364
|
}
|
|
@@ -43,8 +43,10 @@ export interface HealthPlugin {
|
|
|
43
43
|
/**
|
|
44
44
|
* iOS only: Reads user characteristics such as biological sex, blood type, date of birth, Fitzpatrick skin type, and wheelchair use.
|
|
45
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
|
+
* Passing `fields` lets you request only specific characteristics (e.g., date of birth) to keep permissions scoped narrowly. Defaults to all characteristics when omitted.
|
|
46
48
|
*/
|
|
47
|
-
getCharacteristics(): Promise<CharacteristicsResponse>;
|
|
49
|
+
getCharacteristics(request?: CharacteristicsRequest): Promise<CharacteristicsResponse>;
|
|
48
50
|
/**
|
|
49
51
|
* Query aggregated data
|
|
50
52
|
* - Blood-pressure aggregates return the systolic average in `value` plus `systolic`, `diastolic`, and `unit`.
|
|
@@ -206,6 +208,13 @@ export interface CharacteristicsResponse {
|
|
|
206
208
|
*/
|
|
207
209
|
platformMessage?: string;
|
|
208
210
|
}
|
|
211
|
+
export type CharacteristicField = 'biologicalSex' | 'bloodType' | 'dateOfBirth' | 'fitzpatrickSkinType' | 'wheelchairUse';
|
|
212
|
+
export interface CharacteristicsRequest {
|
|
213
|
+
/**
|
|
214
|
+
* Characteristics to query. Defaults to all characteristics when omitted.
|
|
215
|
+
*/
|
|
216
|
+
fields?: CharacteristicField[];
|
|
217
|
+
}
|
|
209
218
|
export type HealthBiologicalSex = 'female' | 'male' | 'other' | 'not_set' | 'unknown';
|
|
210
219
|
export type HealthBloodType = 'a-positive' | 'a-negative' | 'b-positive' | 'b-negative' | 'ab-positive' | 'ab-negative' | 'o-positive' | 'o-negative' | 'not_set' | 'unknown';
|
|
211
220
|
export type HealthFitzpatrickSkinType = 'type1' | 'type2' | 'type3' | 'type4' | 'type5' | 'type6' | '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 * 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 * - `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 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 * 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"]}
|
|
@@ -29,6 +29,29 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
29
29
|
|
|
30
30
|
/// Serial queue to make route‑location mutations thread‑safe without locks
|
|
31
31
|
private let routeSyncQueue = DispatchQueue(label: "com.flomentumsolutions.capacitorhealthextended.routeSync")
|
|
32
|
+
|
|
33
|
+
private enum CharacteristicField: String, CaseIterable {
|
|
34
|
+
case biologicalSex
|
|
35
|
+
case bloodType
|
|
36
|
+
case dateOfBirth
|
|
37
|
+
case fitzpatrickSkinType
|
|
38
|
+
case wheelchairUse
|
|
39
|
+
|
|
40
|
+
var hkObjectType: HKObjectType? {
|
|
41
|
+
switch self {
|
|
42
|
+
case .biologicalSex:
|
|
43
|
+
return HKObjectType.characteristicType(forIdentifier: .biologicalSex)
|
|
44
|
+
case .bloodType:
|
|
45
|
+
return HKObjectType.characteristicType(forIdentifier: .bloodType)
|
|
46
|
+
case .dateOfBirth:
|
|
47
|
+
return HKObjectType.characteristicType(forIdentifier: .dateOfBirth)
|
|
48
|
+
case .fitzpatrickSkinType:
|
|
49
|
+
return HKObjectType.characteristicType(forIdentifier: .fitzpatrickSkinType)
|
|
50
|
+
case .wheelchairUse:
|
|
51
|
+
return HKObjectType.characteristicType(forIdentifier: .wheelchairUse)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
32
55
|
|
|
33
56
|
@objc func isHealthAvailable(_ call: CAPPluginCall) {
|
|
34
57
|
let isAvailable = HKHealthStore.isHealthDataAvailable()
|
|
@@ -114,65 +137,99 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
114
137
|
return
|
|
115
138
|
}
|
|
116
139
|
|
|
117
|
-
let
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
140
|
+
let requestedFields = call.getArray("fields") as? [String] ?? []
|
|
141
|
+
let fieldsToLoad: [CharacteristicField]
|
|
142
|
+
if requestedFields.isEmpty {
|
|
143
|
+
fieldsToLoad = CharacteristicField.allCases
|
|
144
|
+
} else {
|
|
145
|
+
var normalized: [CharacteristicField] = []
|
|
146
|
+
var invalid: [String] = []
|
|
147
|
+
|
|
148
|
+
for name in requestedFields {
|
|
149
|
+
if let field = CharacteristicField(rawValue: name) {
|
|
150
|
+
if !normalized.contains(field) {
|
|
151
|
+
normalized.append(field)
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
invalid.append(name)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if !invalid.isEmpty {
|
|
159
|
+
let validFields = CharacteristicField.allCases.map { $0.rawValue }.joined(separator: ", ")
|
|
160
|
+
call.reject("Invalid characteristic field(s): \(invalid.joined(separator: ", ")). Valid fields are: \(validFields)")
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
fieldsToLoad = normalized.isEmpty ? CharacteristicField.allCases : normalized
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let characteristicTypes: [HKObjectType] = fieldsToLoad.compactMap { $0.hkObjectType }
|
|
168
|
+
|
|
169
|
+
if characteristicTypes.count != fieldsToLoad.count {
|
|
170
|
+
let missing = fieldsToLoad.filter { $0.hkObjectType == nil }.map { $0.rawValue }
|
|
171
|
+
let missingList = missing.joined(separator: ", ")
|
|
172
|
+
let message = missing.isEmpty ? "HealthKit characteristics are unavailable on this device." : "Requested characteristics are unavailable on this device: \(missingList)"
|
|
173
|
+
call.reject(message)
|
|
174
|
+
return
|
|
175
|
+
}
|
|
124
176
|
|
|
125
177
|
guard !characteristicTypes.isEmpty else {
|
|
126
178
|
call.reject("HealthKit characteristics are unavailable on this device.")
|
|
127
179
|
return
|
|
128
180
|
}
|
|
129
181
|
|
|
130
|
-
func resolveCharacteristics() {
|
|
182
|
+
func resolveCharacteristics(for fields: [CharacteristicField]) {
|
|
131
183
|
var result: [String: Any] = [:]
|
|
132
184
|
|
|
133
|
-
func setValue(_ key:
|
|
134
|
-
result[key] = value ?? NSNull()
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if let biologicalSexObject = try? healthStore.biologicalSex() {
|
|
138
|
-
setValue("biologicalSex", mapBiologicalSex(biologicalSexObject.biologicalSex))
|
|
139
|
-
} else {
|
|
140
|
-
setValue("biologicalSex", nil)
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if let bloodTypeObject = try? healthStore.bloodType() {
|
|
144
|
-
setValue("bloodType", mapBloodType(bloodTypeObject.bloodType))
|
|
145
|
-
} else {
|
|
146
|
-
setValue("bloodType", nil)
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if let dateComponents = try? healthStore.dateOfBirthComponents() {
|
|
150
|
-
setValue("dateOfBirth", isoBirthDateString(from: dateComponents))
|
|
151
|
-
} else {
|
|
152
|
-
setValue("dateOfBirth", nil)
|
|
185
|
+
func setValue(_ key: CharacteristicField, _ value: String?) {
|
|
186
|
+
result[key.rawValue] = value ?? NSNull()
|
|
153
187
|
}
|
|
154
188
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
189
|
+
for field in fields {
|
|
190
|
+
switch field {
|
|
191
|
+
case .biologicalSex:
|
|
192
|
+
if let biologicalSexObject = try? healthStore.biologicalSex() {
|
|
193
|
+
setValue(field, mapBiologicalSex(biologicalSexObject.biologicalSex))
|
|
194
|
+
} else {
|
|
195
|
+
setValue(field, nil)
|
|
196
|
+
}
|
|
197
|
+
case .bloodType:
|
|
198
|
+
if let bloodTypeObject = try? healthStore.bloodType() {
|
|
199
|
+
setValue(field, mapBloodType(bloodTypeObject.bloodType))
|
|
200
|
+
} else {
|
|
201
|
+
setValue(field, nil)
|
|
202
|
+
}
|
|
203
|
+
case .dateOfBirth:
|
|
204
|
+
if let dateComponents = try? healthStore.dateOfBirthComponents() {
|
|
205
|
+
setValue(field, isoBirthDateString(from: dateComponents))
|
|
206
|
+
} else {
|
|
207
|
+
setValue(field, nil)
|
|
208
|
+
}
|
|
209
|
+
case .fitzpatrickSkinType:
|
|
210
|
+
if let fitzpatrickObject = try? healthStore.fitzpatrickSkinType() {
|
|
211
|
+
setValue(field, mapFitzpatrickSkinType(fitzpatrickObject.skinType))
|
|
212
|
+
} else {
|
|
213
|
+
setValue(field, nil)
|
|
214
|
+
}
|
|
215
|
+
case .wheelchairUse:
|
|
216
|
+
if let wheelchairUseObject = try? healthStore.wheelchairUse() {
|
|
217
|
+
setValue(field, mapWheelchairUse(wheelchairUseObject.wheelchairUse))
|
|
218
|
+
} else {
|
|
219
|
+
setValue(field, nil)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
165
222
|
}
|
|
166
223
|
|
|
167
224
|
result["platformSupported"] = true
|
|
168
225
|
call.resolve(result)
|
|
169
226
|
}
|
|
170
227
|
|
|
171
|
-
func requestCharacteristicsAccess() {
|
|
172
|
-
self.healthStore.requestAuthorization(toShare: nil, read: Set(
|
|
228
|
+
func requestCharacteristicsAccess(for fields: [CharacteristicField], types: [HKObjectType]) {
|
|
229
|
+
self.healthStore.requestAuthorization(toShare: nil, read: Set(types)) { success, error in
|
|
173
230
|
DispatchQueue.main.async {
|
|
174
231
|
if success {
|
|
175
|
-
resolveCharacteristics()
|
|
232
|
+
resolveCharacteristics(for: fields)
|
|
176
233
|
} else if let error = error {
|
|
177
234
|
call.reject("Authorization failed: \(error.localizedDescription)")
|
|
178
235
|
} else {
|
|
@@ -194,9 +251,9 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
194
251
|
|
|
195
252
|
switch status {
|
|
196
253
|
case .shouldRequest, .unknown:
|
|
197
|
-
requestCharacteristicsAccess()
|
|
254
|
+
requestCharacteristicsAccess(for: fieldsToLoad, types: characteristicTypes)
|
|
198
255
|
default:
|
|
199
|
-
resolveCharacteristics()
|
|
256
|
+
resolveCharacteristics(for: fieldsToLoad)
|
|
200
257
|
}
|
|
201
258
|
}
|
|
202
259
|
}
|
|
@@ -892,9 +949,17 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
892
949
|
return [HKObjectType.quantityType(forIdentifier: .heartRate)].compactMap { $0 as? HKSampleType }
|
|
893
950
|
case "WRITE_ROUTE":
|
|
894
951
|
return [HKSeriesType.workoutRoute()].compactMap { $0 as? HKSampleType }
|
|
952
|
+
case "WRITE_WEIGHT":
|
|
953
|
+
return [HKObjectType.quantityType(forIdentifier: .bodyMass)].compactMap { $0 as? HKSampleType }
|
|
954
|
+
case "WRITE_HEIGHT":
|
|
955
|
+
return [HKObjectType.quantityType(forIdentifier: .height)].compactMap { $0 as? HKSampleType }
|
|
956
|
+
case "WRITE_BODY_FAT":
|
|
957
|
+
return [HKObjectType.quantityType(forIdentifier: .bodyFatPercentage)].compactMap { $0 as? HKSampleType }
|
|
958
|
+
case "WRITE_RESTING_HEART_RATE":
|
|
959
|
+
return [HKObjectType.quantityType(forIdentifier: .restingHeartRate)].compactMap { $0 as? HKSampleType }
|
|
895
960
|
default:
|
|
896
|
-
//
|
|
897
|
-
return
|
|
961
|
+
// Avoid requesting write/share authorization when only read permissions are supplied.
|
|
962
|
+
return []
|
|
898
963
|
}
|
|
899
964
|
}
|
|
900
965
|
|
package/package.json
CHANGED