@capgo/capacitor-health 8.4.4 → 8.4.6
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 +1 -1
- package/android/src/main/java/app/capgo/plugin/health/HealthManager.kt +17 -0
- package/dist/docs.json +4 -0
- package/dist/esm/definitions.d.ts +1 -1
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/HealthPlugin/Health.swift +64 -31
- package/ios/Sources/HealthPlugin/HealthPlugin.swift +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -655,7 +655,7 @@ Supported on iOS (HealthKit) and Android (Health Connect).
|
|
|
655
655
|
|
|
656
656
|
#### HealthDataType
|
|
657
657
|
|
|
658
|
-
<code>'steps' | 'distance' | 'calories' | 'heartRate' | 'weight' | 'sleep' | 'respiratoryRate' | 'oxygenSaturation' | 'restingHeartRate' | 'heartRateVariability' | 'bloodPressure' | 'bloodGlucose' | 'bodyTemperature' | 'height' | 'flightsClimbed' | 'exerciseTime' | 'distanceCycling' | 'bodyFat' | 'basalBodyTemperature' | 'basalCalories' | 'totalCalories' | 'mindfulness'</code>
|
|
658
|
+
<code>'steps' | 'distance' | 'calories' | 'heartRate' | 'weight' | 'sleep' | 'respiratoryRate' | 'oxygenSaturation' | 'restingHeartRate' | 'heartRateVariability' | 'bloodPressure' | 'bloodGlucose' | 'bodyTemperature' | 'height' | 'flightsClimbed' | 'exerciseTime' | 'distanceCycling' | 'bodyFat' | 'basalBodyTemperature' | 'basalCalories' | 'totalCalories' | 'mindfulness' | 'workouts'</code>
|
|
659
659
|
|
|
660
660
|
|
|
661
661
|
#### HealthUnit
|
|
@@ -865,6 +865,23 @@ class HealthManager {
|
|
|
865
865
|
// Other errors (e.g., no data available)
|
|
866
866
|
android.util.Log.d("HealthManager", "Workout data aggregation failed: ${e.message}", e)
|
|
867
867
|
}
|
|
868
|
+
|
|
869
|
+
if (caloriesAggregate == null) {
|
|
870
|
+
try {
|
|
871
|
+
val fallbackRequest = AggregateRequest(
|
|
872
|
+
metrics = setOf(TotalCaloriesBurnedRecord.ENERGY_TOTAL),
|
|
873
|
+
timeRangeFilter = timeRange
|
|
874
|
+
)
|
|
875
|
+
val fallbackResult = client.aggregate(fallbackRequest)
|
|
876
|
+
caloriesAggregate = fallbackResult[TotalCaloriesBurnedRecord.ENERGY_TOTAL]?.inKilocalories
|
|
877
|
+
} catch (e: CancellationException) {
|
|
878
|
+
throw e
|
|
879
|
+
} catch (e: SecurityException) {
|
|
880
|
+
android.util.Log.d("HealthManager", "Permission denied for workout total calories aggregation: ${e.message}", e)
|
|
881
|
+
} catch (e: Exception) {
|
|
882
|
+
android.util.Log.d("HealthManager", "Workout total calories aggregation failed: ${e.message}", e)
|
|
883
|
+
}
|
|
884
|
+
}
|
|
868
885
|
|
|
869
886
|
return WorkoutAggregatedData(
|
|
870
887
|
totalDistance = distanceAggregate,
|
package/dist/docs.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type HealthDataType = 'steps' | 'distance' | 'calories' | 'heartRate' | 'weight' | 'sleep' | 'respiratoryRate' | 'oxygenSaturation' | 'restingHeartRate' | 'heartRateVariability' | 'bloodPressure' | 'bloodGlucose' | 'bodyTemperature' | 'height' | 'flightsClimbed' | 'exerciseTime' | 'distanceCycling' | 'bodyFat' | 'basalBodyTemperature' | 'basalCalories' | 'totalCalories' | 'mindfulness';
|
|
1
|
+
export type HealthDataType = 'steps' | 'distance' | 'calories' | 'heartRate' | 'weight' | 'sleep' | 'respiratoryRate' | 'oxygenSaturation' | 'restingHeartRate' | 'heartRateVariability' | 'bloodPressure' | 'bloodGlucose' | 'bodyTemperature' | 'height' | 'flightsClimbed' | 'exerciseTime' | 'distanceCycling' | 'bodyFat' | 'basalBodyTemperature' | 'basalCalories' | 'totalCalories' | 'mindfulness' | 'workouts';
|
|
2
2
|
export type HealthUnit = 'count' | 'meter' | 'kilocalorie' | 'bpm' | 'kilogram' | 'minute' | 'percent' | 'millisecond' | 'mmHg' | 'mg/dL' | 'celsius' | 'fahrenheit' | 'centimeter';
|
|
3
3
|
export interface AuthorizationOptions {
|
|
4
4
|
/** Data types that should be readable after authorization. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["export type HealthDataType =\n | 'steps'\n | 'distance'\n | 'calories'\n | 'heartRate'\n | 'weight'\n | 'sleep'\n | 'respiratoryRate'\n | 'oxygenSaturation'\n | 'restingHeartRate'\n | 'heartRateVariability'\n | 'bloodPressure'\n | 'bloodGlucose'\n | 'bodyTemperature'\n | 'height'\n | 'flightsClimbed'\n | 'exerciseTime'\n | 'distanceCycling'\n | 'bodyFat'\n | 'basalBodyTemperature'\n | 'basalCalories'\n | 'totalCalories'\n | 'mindfulness';\n\nexport type HealthUnit =\n | 'count'\n | 'meter'\n | 'kilocalorie'\n | 'bpm'\n | 'kilogram'\n | 'minute'\n | 'percent'\n | 'millisecond'\n | 'mmHg'\n | 'mg/dL'\n | 'celsius'\n | 'fahrenheit'\n | 'centimeter';\n\nexport interface AuthorizationOptions {\n /** Data types that should be readable after authorization. */\n read?: HealthDataType[];\n /** Data types that should be writable after authorization. */\n write?: HealthDataType[];\n}\n\nexport interface AuthorizationStatus {\n readAuthorized: HealthDataType[];\n readDenied: HealthDataType[];\n writeAuthorized: HealthDataType[];\n writeDenied: HealthDataType[];\n}\n\nexport interface AvailabilityResult {\n available: boolean;\n /** Platform specific details (for debugging/diagnostics). */\n platform?: 'ios' | 'android' | 'web';\n reason?: string;\n}\n\nexport interface QueryOptions {\n /** The type of data to retrieve from the health store. */\n dataType: HealthDataType;\n /** Inclusive ISO 8601 start date (defaults to now - 1 day). */\n startDate?: string;\n /** Exclusive ISO 8601 end date (defaults to now). */\n endDate?: string;\n /** Maximum number of samples to return (defaults to 100). */\n limit?: number;\n /** Return results sorted ascending by start date (defaults to false). */\n ascending?: boolean;\n}\n\nexport type SleepState = 'inBed' | 'asleep' | 'awake' | 'rem' | 'deep' | 'light';\n\nexport interface HealthSample {\n dataType: HealthDataType;\n value: number;\n unit: HealthUnit;\n startDate: string;\n endDate: string;\n sourceName?: string;\n sourceId?: string;\n /** Platform-specific unique identifier (HealthKit UUID on iOS, Health Connect metadata ID on Android). */\n platformId?: string;\n /** For sleep data, indicates the sleep state (e.g., 'asleep', 'awake', 'rem', 'deep', 'light'). */\n sleepState?: SleepState;\n /** For blood pressure data, the systolic value in mmHg. */\n systolic?: number;\n /** For blood pressure data, the diastolic value in mmHg. */\n diastolic?: number;\n}\n\nexport interface ReadSamplesResult {\n samples: HealthSample[];\n}\n\nexport type WorkoutType =\n // Common types (supported on both platforms)\n | 'americanFootball'\n | 'australianFootball'\n | 'badminton'\n | 'baseball'\n | 'basketball'\n | 'bowling'\n | 'boxing'\n | 'climbing'\n | 'cricket'\n | 'crossTraining'\n | 'curling'\n | 'cycling'\n | 'dance'\n | 'elliptical'\n | 'fencing'\n | 'functionalStrengthTraining'\n | 'golf'\n | 'gymnastics'\n | 'handball'\n | 'hiking'\n | 'hockey'\n | 'jumpRope'\n | 'kickboxing'\n | 'lacrosse'\n | 'martialArts'\n | 'pilates'\n | 'racquetball'\n | 'rowing'\n | 'rugby'\n | 'running'\n | 'sailing'\n | 'skatingSports'\n | 'skiing'\n | 'snowboarding'\n | 'soccer'\n | 'softball'\n | 'squash'\n | 'stairClimbing'\n | 'strengthTraining'\n | 'surfing'\n | 'swimming'\n | 'swimmingPool'\n | 'swimmingOpenWater'\n | 'tableTennis'\n | 'tennis'\n | 'trackAndField'\n | 'traditionalStrengthTraining'\n | 'volleyball'\n | 'walking'\n | 'waterFitness'\n | 'waterPolo'\n | 'waterSports'\n | 'weightlifting'\n | 'wheelchair'\n | 'yoga'\n // iOS specific types\n | 'archery'\n | 'barre'\n | 'cooldown'\n | 'coreTraining'\n | 'crossCountrySkiing'\n | 'discSports'\n | 'downhillSkiing'\n | 'equestrianSports'\n | 'fishing'\n | 'fitnessGaming'\n | 'flexibility'\n | 'handCycling'\n | 'highIntensityIntervalTraining'\n | 'hunting'\n | 'mindAndBody'\n | 'mixedCardio'\n | 'paddleSports'\n | 'pickleball'\n | 'play'\n | 'preparationAndRecovery'\n | 'snowSports'\n | 'stairs'\n | 'stepTraining'\n | 'surfingSports'\n | 'taiChi'\n | 'transition'\n | 'underwaterDiving'\n | 'wheelchairRunPace'\n | 'wheelchairWalkPace'\n | 'wrestling'\n | 'cardioDance'\n | 'socialDance'\n // Android specific types\n | 'backExtension'\n | 'barbellShoulderPress'\n | 'benchPress'\n | 'benchSitUp'\n | 'bikingStationary'\n | 'bootCamp'\n | 'burpee'\n | 'calisthenics'\n | 'crunch'\n | 'dancing'\n | 'deadlift'\n | 'dumbbellCurlLeftArm'\n | 'dumbbellCurlRightArm'\n | 'dumbbellFrontRaise'\n | 'dumbbellLateralRaise'\n | 'dumbbellTricepsExtensionLeftArm'\n | 'dumbbellTricepsExtensionRightArm'\n | 'dumbbellTricepsExtensionTwoArm'\n | 'exerciseClass'\n | 'forwardTwist'\n | 'frisbeedisc'\n | 'guidedBreathing'\n | 'iceHockey'\n | 'iceSkating'\n | 'jumpingJack'\n | 'latPullDown'\n | 'lunge'\n | 'meditation'\n | 'paddling'\n | 'paraGliding'\n | 'plank'\n | 'rockClimbing'\n | 'rollerHockey'\n | 'rowingMachine'\n | 'runningTreadmill'\n | 'scubaDiving'\n | 'skating'\n | 'snowshoeing'\n | 'stairClimbingMachine'\n | 'stretching'\n | 'upperTwist'\n | 'other';\n\nexport interface QueryWorkoutsOptions {\n /** Optional workout type filter. If omitted, all workout types are returned. */\n workoutType?: WorkoutType;\n /** Inclusive ISO 8601 start date (defaults to now - 1 day). */\n startDate?: string;\n /** Exclusive ISO 8601 end date (defaults to now). */\n endDate?: string;\n /** Maximum number of workouts to return (defaults to 100). */\n limit?: number;\n /** Return results sorted ascending by start date (defaults to false). */\n ascending?: boolean;\n /**\n * Anchor for pagination. Use the anchor returned from a previous query to continue from that point.\n * On iOS, this is the ISO 8601 cursor returned by the previous query. On Android, this uses\n * Health Connect's pageToken.\n * Omit this parameter to start from the beginning.\n */\n anchor?: string;\n}\n\nexport interface Workout {\n /** The type of workout. */\n workoutType: WorkoutType;\n /** Duration of the workout in seconds. */\n duration: number;\n /** Total energy burned in kilocalories (if available). */\n totalEnergyBurned?: number;\n /** Total distance in meters (if available). */\n totalDistance?: number;\n /** ISO 8601 start date of the workout. */\n startDate: string;\n /** ISO 8601 end date of the workout. */\n endDate: string;\n /** Source name that recorded the workout. */\n sourceName?: string;\n /** Source bundle identifier. */\n sourceId?: string;\n /** Platform-specific unique identifier (HealthKit UUID on iOS, Health Connect metadata ID on Android). */\n platformId?: string;\n /** Additional metadata (if available). */\n metadata?: Record<string, string>;\n}\n\nexport interface QueryWorkoutsResult {\n workouts: Workout[];\n /**\n * Anchor for the next page of results. Pass this value as the anchor parameter in the next query\n * to continue pagination. If undefined or null, there are no more results.\n */\n anchor?: string;\n}\n\nexport interface WriteSampleOptions {\n dataType: HealthDataType;\n value: number;\n /**\n * Optional unit override. If omitted, the default unit for the data type is used\n * (count for `steps`, meter for `distance`, kilocalorie for `calories`, bpm for `heartRate`, kilogram for `weight`).\n */\n unit?: HealthUnit;\n /** ISO 8601 start date for the sample. Defaults to now. */\n startDate?: string;\n /** ISO 8601 end date for the sample. Defaults to startDate. */\n endDate?: string;\n /** Metadata key-value pairs forwarded to the native APIs where supported. */\n metadata?: Record<string, string>;\n /** For blood pressure data, the systolic value in mmHg. Required when dataType is 'bloodPressure'. */\n systolic?: number;\n /** For blood pressure data, the diastolic value in mmHg. Required when dataType is 'bloodPressure'. */\n diastolic?: number;\n}\n\nexport type BucketType = 'hour' | 'day' | 'week' | 'month';\n\nexport type AggregationType = 'sum' | 'average' | 'min' | 'max';\n\nexport interface QueryAggregatedOptions {\n /** The type of data to aggregate from the health store. */\n dataType: HealthDataType;\n /** Inclusive ISO 8601 start date (defaults to now - 1 day). */\n startDate?: string;\n /** Exclusive ISO 8601 end date (defaults to now). */\n endDate?: string;\n /** Time bucket for aggregation (defaults to 'day'). */\n bucket?: BucketType;\n /** Aggregation operation to perform (defaults to 'sum'). */\n aggregation?: AggregationType;\n}\n\nexport interface AggregatedSample {\n /** ISO 8601 start date of the bucket. */\n startDate: string;\n /** ISO 8601 end date of the bucket. */\n endDate: string;\n /** Aggregated value for the bucket. */\n value: number;\n /** Unit of the aggregated value. */\n unit: HealthUnit;\n}\n\nexport interface QueryAggregatedResult {\n samples: AggregatedSample[];\n}\n\nexport interface HealthPlugin {\n /** Returns whether the current platform supports the native health SDK. */\n isAvailable(): Promise<AvailabilityResult>;\n /** Requests read/write access to the provided data types. */\n requestAuthorization(options: AuthorizationOptions): Promise<AuthorizationStatus>;\n /** Checks authorization status for the provided data types without prompting the user. */\n checkAuthorization(options: AuthorizationOptions): Promise<AuthorizationStatus>;\n /** Reads samples for the given data type within the specified time frame. */\n readSamples(options: QueryOptions): Promise<ReadSamplesResult>;\n /** Writes a single sample to the native health store. */\n saveSample(options: WriteSampleOptions): Promise<void>;\n\n /**\n * Get the native Capacitor plugin version\n *\n * @returns {Promise<{ version: string }>} a Promise with version for this device\n * @throws An error if something went wrong\n */\n getPluginVersion(): Promise<{ version: string }>;\n\n /**\n * Opens the Health Connect settings screen (Android only).\n * On iOS, this method does nothing.\n *\n * Use this to direct users to manage their Health Connect permissions\n * or to install Health Connect if not available.\n *\n * @throws An error if Health Connect settings cannot be opened\n */\n openHealthConnectSettings(): Promise<void>;\n\n /**\n * Shows the app's privacy policy for Health Connect (Android only).\n * On iOS, this method does nothing.\n *\n * This displays the same privacy policy screen that Health Connect shows\n * when the user taps \"Privacy policy\" in the permissions dialog.\n *\n * The privacy policy URL can be configured by adding a string resource\n * named \"health_connect_privacy_policy_url\" in your app's strings.xml,\n * or by placing an HTML file at www/privacypolicy.html in your assets.\n *\n * @throws An error if the privacy policy cannot be displayed\n */\n showPrivacyPolicy(): Promise<void>;\n\n /**\n * Queries workout sessions from the native health store.\n * Supported on iOS (HealthKit) and Android (Health Connect).\n *\n * @param options Query options including optional workout type filter, date range, limit, and sort order\n * @returns A promise that resolves with the workout sessions\n * @throws An error if something went wrong\n */\n queryWorkouts(options: QueryWorkoutsOptions): Promise<QueryWorkoutsResult>;\n\n /**\n * Queries aggregated health data from the native health store.\n * Aggregates data into time buckets (hour, day, week, month) with operations like sum, average, min, or max.\n * This is more efficient than fetching individual samples for large date ranges.\n *\n * Supported on iOS (HealthKit) and Android (Health Connect).\n *\n * @param options Query options including data type, date range, bucket size, and aggregation type\n * @returns A promise that resolves with the aggregated samples\n * @throws An error if something went wrong\n */\n queryAggregated(options: QueryAggregatedOptions): Promise<QueryAggregatedResult>;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["export type HealthDataType =\n | 'steps'\n | 'distance'\n | 'calories'\n | 'heartRate'\n | 'weight'\n | 'sleep'\n | 'respiratoryRate'\n | 'oxygenSaturation'\n | 'restingHeartRate'\n | 'heartRateVariability'\n | 'bloodPressure'\n | 'bloodGlucose'\n | 'bodyTemperature'\n | 'height'\n | 'flightsClimbed'\n | 'exerciseTime'\n | 'distanceCycling'\n | 'bodyFat'\n | 'basalBodyTemperature'\n | 'basalCalories'\n | 'totalCalories'\n | 'mindfulness'\n | 'workouts';\n\nexport type HealthUnit =\n | 'count'\n | 'meter'\n | 'kilocalorie'\n | 'bpm'\n | 'kilogram'\n | 'minute'\n | 'percent'\n | 'millisecond'\n | 'mmHg'\n | 'mg/dL'\n | 'celsius'\n | 'fahrenheit'\n | 'centimeter';\n\nexport interface AuthorizationOptions {\n /** Data types that should be readable after authorization. */\n read?: HealthDataType[];\n /** Data types that should be writable after authorization. */\n write?: HealthDataType[];\n}\n\nexport interface AuthorizationStatus {\n readAuthorized: HealthDataType[];\n readDenied: HealthDataType[];\n writeAuthorized: HealthDataType[];\n writeDenied: HealthDataType[];\n}\n\nexport interface AvailabilityResult {\n available: boolean;\n /** Platform specific details (for debugging/diagnostics). */\n platform?: 'ios' | 'android' | 'web';\n reason?: string;\n}\n\nexport interface QueryOptions {\n /** The type of data to retrieve from the health store. */\n dataType: HealthDataType;\n /** Inclusive ISO 8601 start date (defaults to now - 1 day). */\n startDate?: string;\n /** Exclusive ISO 8601 end date (defaults to now). */\n endDate?: string;\n /** Maximum number of samples to return (defaults to 100). */\n limit?: number;\n /** Return results sorted ascending by start date (defaults to false). */\n ascending?: boolean;\n}\n\nexport type SleepState = 'inBed' | 'asleep' | 'awake' | 'rem' | 'deep' | 'light';\n\nexport interface HealthSample {\n dataType: HealthDataType;\n value: number;\n unit: HealthUnit;\n startDate: string;\n endDate: string;\n sourceName?: string;\n sourceId?: string;\n /** Platform-specific unique identifier (HealthKit UUID on iOS, Health Connect metadata ID on Android). */\n platformId?: string;\n /** For sleep data, indicates the sleep state (e.g., 'asleep', 'awake', 'rem', 'deep', 'light'). */\n sleepState?: SleepState;\n /** For blood pressure data, the systolic value in mmHg. */\n systolic?: number;\n /** For blood pressure data, the diastolic value in mmHg. */\n diastolic?: number;\n}\n\nexport interface ReadSamplesResult {\n samples: HealthSample[];\n}\n\nexport type WorkoutType =\n // Common types (supported on both platforms)\n | 'americanFootball'\n | 'australianFootball'\n | 'badminton'\n | 'baseball'\n | 'basketball'\n | 'bowling'\n | 'boxing'\n | 'climbing'\n | 'cricket'\n | 'crossTraining'\n | 'curling'\n | 'cycling'\n | 'dance'\n | 'elliptical'\n | 'fencing'\n | 'functionalStrengthTraining'\n | 'golf'\n | 'gymnastics'\n | 'handball'\n | 'hiking'\n | 'hockey'\n | 'jumpRope'\n | 'kickboxing'\n | 'lacrosse'\n | 'martialArts'\n | 'pilates'\n | 'racquetball'\n | 'rowing'\n | 'rugby'\n | 'running'\n | 'sailing'\n | 'skatingSports'\n | 'skiing'\n | 'snowboarding'\n | 'soccer'\n | 'softball'\n | 'squash'\n | 'stairClimbing'\n | 'strengthTraining'\n | 'surfing'\n | 'swimming'\n | 'swimmingPool'\n | 'swimmingOpenWater'\n | 'tableTennis'\n | 'tennis'\n | 'trackAndField'\n | 'traditionalStrengthTraining'\n | 'volleyball'\n | 'walking'\n | 'waterFitness'\n | 'waterPolo'\n | 'waterSports'\n | 'weightlifting'\n | 'wheelchair'\n | 'yoga'\n // iOS specific types\n | 'archery'\n | 'barre'\n | 'cooldown'\n | 'coreTraining'\n | 'crossCountrySkiing'\n | 'discSports'\n | 'downhillSkiing'\n | 'equestrianSports'\n | 'fishing'\n | 'fitnessGaming'\n | 'flexibility'\n | 'handCycling'\n | 'highIntensityIntervalTraining'\n | 'hunting'\n | 'mindAndBody'\n | 'mixedCardio'\n | 'paddleSports'\n | 'pickleball'\n | 'play'\n | 'preparationAndRecovery'\n | 'snowSports'\n | 'stairs'\n | 'stepTraining'\n | 'surfingSports'\n | 'taiChi'\n | 'transition'\n | 'underwaterDiving'\n | 'wheelchairRunPace'\n | 'wheelchairWalkPace'\n | 'wrestling'\n | 'cardioDance'\n | 'socialDance'\n // Android specific types\n | 'backExtension'\n | 'barbellShoulderPress'\n | 'benchPress'\n | 'benchSitUp'\n | 'bikingStationary'\n | 'bootCamp'\n | 'burpee'\n | 'calisthenics'\n | 'crunch'\n | 'dancing'\n | 'deadlift'\n | 'dumbbellCurlLeftArm'\n | 'dumbbellCurlRightArm'\n | 'dumbbellFrontRaise'\n | 'dumbbellLateralRaise'\n | 'dumbbellTricepsExtensionLeftArm'\n | 'dumbbellTricepsExtensionRightArm'\n | 'dumbbellTricepsExtensionTwoArm'\n | 'exerciseClass'\n | 'forwardTwist'\n | 'frisbeedisc'\n | 'guidedBreathing'\n | 'iceHockey'\n | 'iceSkating'\n | 'jumpingJack'\n | 'latPullDown'\n | 'lunge'\n | 'meditation'\n | 'paddling'\n | 'paraGliding'\n | 'plank'\n | 'rockClimbing'\n | 'rollerHockey'\n | 'rowingMachine'\n | 'runningTreadmill'\n | 'scubaDiving'\n | 'skating'\n | 'snowshoeing'\n | 'stairClimbingMachine'\n | 'stretching'\n | 'upperTwist'\n | 'other';\n\nexport interface QueryWorkoutsOptions {\n /** Optional workout type filter. If omitted, all workout types are returned. */\n workoutType?: WorkoutType;\n /** Inclusive ISO 8601 start date (defaults to now - 1 day). */\n startDate?: string;\n /** Exclusive ISO 8601 end date (defaults to now). */\n endDate?: string;\n /** Maximum number of workouts to return (defaults to 100). */\n limit?: number;\n /** Return results sorted ascending by start date (defaults to false). */\n ascending?: boolean;\n /**\n * Anchor for pagination. Use the anchor returned from a previous query to continue from that point.\n * On iOS, this is the ISO 8601 cursor returned by the previous query. On Android, this uses\n * Health Connect's pageToken.\n * Omit this parameter to start from the beginning.\n */\n anchor?: string;\n}\n\nexport interface Workout {\n /** The type of workout. */\n workoutType: WorkoutType;\n /** Duration of the workout in seconds. */\n duration: number;\n /** Total energy burned in kilocalories (if available). */\n totalEnergyBurned?: number;\n /** Total distance in meters (if available). */\n totalDistance?: number;\n /** ISO 8601 start date of the workout. */\n startDate: string;\n /** ISO 8601 end date of the workout. */\n endDate: string;\n /** Source name that recorded the workout. */\n sourceName?: string;\n /** Source bundle identifier. */\n sourceId?: string;\n /** Platform-specific unique identifier (HealthKit UUID on iOS, Health Connect metadata ID on Android). */\n platformId?: string;\n /** Additional metadata (if available). */\n metadata?: Record<string, string>;\n}\n\nexport interface QueryWorkoutsResult {\n workouts: Workout[];\n /**\n * Anchor for the next page of results. Pass this value as the anchor parameter in the next query\n * to continue pagination. If undefined or null, there are no more results.\n */\n anchor?: string;\n}\n\nexport interface WriteSampleOptions {\n dataType: HealthDataType;\n value: number;\n /**\n * Optional unit override. If omitted, the default unit for the data type is used\n * (count for `steps`, meter for `distance`, kilocalorie for `calories`, bpm for `heartRate`, kilogram for `weight`).\n */\n unit?: HealthUnit;\n /** ISO 8601 start date for the sample. Defaults to now. */\n startDate?: string;\n /** ISO 8601 end date for the sample. Defaults to startDate. */\n endDate?: string;\n /** Metadata key-value pairs forwarded to the native APIs where supported. */\n metadata?: Record<string, string>;\n /** For blood pressure data, the systolic value in mmHg. Required when dataType is 'bloodPressure'. */\n systolic?: number;\n /** For blood pressure data, the diastolic value in mmHg. Required when dataType is 'bloodPressure'. */\n diastolic?: number;\n}\n\nexport type BucketType = 'hour' | 'day' | 'week' | 'month';\n\nexport type AggregationType = 'sum' | 'average' | 'min' | 'max';\n\nexport interface QueryAggregatedOptions {\n /** The type of data to aggregate from the health store. */\n dataType: HealthDataType;\n /** Inclusive ISO 8601 start date (defaults to now - 1 day). */\n startDate?: string;\n /** Exclusive ISO 8601 end date (defaults to now). */\n endDate?: string;\n /** Time bucket for aggregation (defaults to 'day'). */\n bucket?: BucketType;\n /** Aggregation operation to perform (defaults to 'sum'). */\n aggregation?: AggregationType;\n}\n\nexport interface AggregatedSample {\n /** ISO 8601 start date of the bucket. */\n startDate: string;\n /** ISO 8601 end date of the bucket. */\n endDate: string;\n /** Aggregated value for the bucket. */\n value: number;\n /** Unit of the aggregated value. */\n unit: HealthUnit;\n}\n\nexport interface QueryAggregatedResult {\n samples: AggregatedSample[];\n}\n\nexport interface HealthPlugin {\n /** Returns whether the current platform supports the native health SDK. */\n isAvailable(): Promise<AvailabilityResult>;\n /** Requests read/write access to the provided data types. */\n requestAuthorization(options: AuthorizationOptions): Promise<AuthorizationStatus>;\n /** Checks authorization status for the provided data types without prompting the user. */\n checkAuthorization(options: AuthorizationOptions): Promise<AuthorizationStatus>;\n /** Reads samples for the given data type within the specified time frame. */\n readSamples(options: QueryOptions): Promise<ReadSamplesResult>;\n /** Writes a single sample to the native health store. */\n saveSample(options: WriteSampleOptions): Promise<void>;\n\n /**\n * Get the native Capacitor plugin version\n *\n * @returns {Promise<{ version: string }>} a Promise with version for this device\n * @throws An error if something went wrong\n */\n getPluginVersion(): Promise<{ version: string }>;\n\n /**\n * Opens the Health Connect settings screen (Android only).\n * On iOS, this method does nothing.\n *\n * Use this to direct users to manage their Health Connect permissions\n * or to install Health Connect if not available.\n *\n * @throws An error if Health Connect settings cannot be opened\n */\n openHealthConnectSettings(): Promise<void>;\n\n /**\n * Shows the app's privacy policy for Health Connect (Android only).\n * On iOS, this method does nothing.\n *\n * This displays the same privacy policy screen that Health Connect shows\n * when the user taps \"Privacy policy\" in the permissions dialog.\n *\n * The privacy policy URL can be configured by adding a string resource\n * named \"health_connect_privacy_policy_url\" in your app's strings.xml,\n * or by placing an HTML file at www/privacypolicy.html in your assets.\n *\n * @throws An error if the privacy policy cannot be displayed\n */\n showPrivacyPolicy(): Promise<void>;\n\n /**\n * Queries workout sessions from the native health store.\n * Supported on iOS (HealthKit) and Android (Health Connect).\n *\n * @param options Query options including optional workout type filter, date range, limit, and sort order\n * @returns A promise that resolves with the workout sessions\n * @throws An error if something went wrong\n */\n queryWorkouts(options: QueryWorkoutsOptions): Promise<QueryWorkoutsResult>;\n\n /**\n * Queries aggregated health data from the native health store.\n * Aggregates data into time buckets (hour, day, week, month) with operations like sum, average, min, or max.\n * This is more efficient than fetching individual samples for large date ranges.\n *\n * Supported on iOS (HealthKit) and Android (Health Connect).\n *\n * @param options Query options including data type, date range, bucket size, and aggregation type\n * @returns A promise that resolves with the aggregated samples\n * @throws An error if something went wrong\n */\n queryAggregated(options: QueryAggregatedOptions): Promise<QueryAggregatedResult>;\n}\n"]}
|
|
@@ -754,17 +754,17 @@ enum HealthDataType: String, CaseIterable {
|
|
|
754
754
|
}
|
|
755
755
|
|
|
756
756
|
struct AuthorizationStatusPayload {
|
|
757
|
-
let readAuthorized: [
|
|
758
|
-
let readDenied: [
|
|
759
|
-
let writeAuthorized: [
|
|
760
|
-
let writeDenied: [
|
|
757
|
+
let readAuthorized: [String]
|
|
758
|
+
let readDenied: [String]
|
|
759
|
+
let writeAuthorized: [String]
|
|
760
|
+
let writeDenied: [String]
|
|
761
761
|
|
|
762
762
|
func toDictionary() -> [String: Any] {
|
|
763
763
|
return [
|
|
764
|
-
"readAuthorized": readAuthorized
|
|
765
|
-
"readDenied": readDenied
|
|
766
|
-
"writeAuthorized": writeAuthorized
|
|
767
|
-
"writeDenied": writeDenied
|
|
764
|
+
"readAuthorized": readAuthorized,
|
|
765
|
+
"readDenied": readDenied,
|
|
766
|
+
"writeAuthorized": writeAuthorized,
|
|
767
|
+
"writeDenied": writeDenied
|
|
768
768
|
]
|
|
769
769
|
}
|
|
770
770
|
}
|
|
@@ -772,6 +772,7 @@ struct AuthorizationStatusPayload {
|
|
|
772
772
|
final class Health {
|
|
773
773
|
private let healthStore = HKHealthStore()
|
|
774
774
|
private let isoFormatter: ISO8601DateFormatter
|
|
775
|
+
private let emptyWriteTypesSet = Set<HKSampleType>()
|
|
775
776
|
|
|
776
777
|
/// Small time offset used to move the workout pagination cursor past the last item of a page.
|
|
777
778
|
private let paginationOffsetSeconds: TimeInterval = 0.001
|
|
@@ -809,7 +810,7 @@ final class Health {
|
|
|
809
810
|
let (readTypes, includeWorkouts) = try parseTypesWithWorkouts(readIdentifiers)
|
|
810
811
|
let writeTypes = try HealthDataType.parseMany(writeIdentifiers)
|
|
811
812
|
|
|
812
|
-
var readObjectTypes = try
|
|
813
|
+
var readObjectTypes = try readAuthorizationObjectTypes(for: readTypes)
|
|
813
814
|
// Include workout type if explicitly requested
|
|
814
815
|
if includeWorkouts {
|
|
815
816
|
readObjectTypes.insert(HKObjectType.workoutType())
|
|
@@ -825,7 +826,7 @@ final class Health {
|
|
|
825
826
|
}
|
|
826
827
|
|
|
827
828
|
if success {
|
|
828
|
-
self.evaluateAuthorizationStatus(readTypes: readTypes, writeTypes: writeTypes) { result in
|
|
829
|
+
self.evaluateAuthorizationStatus(readTypes: readTypes, includeWorkouts: includeWorkouts, writeTypes: writeTypes) { result in
|
|
829
830
|
completion(.success(result))
|
|
830
831
|
}
|
|
831
832
|
} else {
|
|
@@ -839,10 +840,10 @@ final class Health {
|
|
|
839
840
|
|
|
840
841
|
func checkAuthorization(readIdentifiers: [String], writeIdentifiers: [String], completion: @escaping (Result<AuthorizationStatusPayload, Error>) -> Void) {
|
|
841
842
|
do {
|
|
842
|
-
let (readTypes,
|
|
843
|
+
let (readTypes, includeWorkouts) = try parseTypesWithWorkouts(readIdentifiers)
|
|
843
844
|
let writeTypes = try HealthDataType.parseMany(writeIdentifiers)
|
|
844
845
|
|
|
845
|
-
evaluateAuthorizationStatus(readTypes: readTypes, writeTypes: writeTypes) { payload in
|
|
846
|
+
evaluateAuthorizationStatus(readTypes: readTypes, includeWorkouts: includeWorkouts, writeTypes: writeTypes) { payload in
|
|
846
847
|
completion(.success(payload))
|
|
847
848
|
}
|
|
848
849
|
} catch {
|
|
@@ -1186,15 +1187,15 @@ final class Health {
|
|
|
1186
1187
|
}
|
|
1187
1188
|
}
|
|
1188
1189
|
|
|
1189
|
-
private func evaluateAuthorizationStatus(readTypes: [HealthDataType], writeTypes: [HealthDataType], completion: @escaping (AuthorizationStatusPayload) -> Void) {
|
|
1190
|
+
private func evaluateAuthorizationStatus(readTypes: [HealthDataType], includeWorkouts: Bool, writeTypes: [HealthDataType], completion: @escaping (AuthorizationStatusPayload) -> Void) {
|
|
1190
1191
|
let writeStatus = writeAuthorizationStatus(for: writeTypes)
|
|
1191
1192
|
|
|
1192
|
-
readAuthorizationStatus(for: readTypes) { readAuthorized, readDenied in
|
|
1193
|
+
readAuthorizationStatus(for: readTypes, includeWorkouts: includeWorkouts) { readAuthorized, readDenied in
|
|
1193
1194
|
let payload = AuthorizationStatusPayload(
|
|
1194
1195
|
readAuthorized: readAuthorized,
|
|
1195
1196
|
readDenied: readDenied,
|
|
1196
|
-
writeAuthorized: writeStatus.authorized,
|
|
1197
|
-
writeDenied: writeStatus.denied
|
|
1197
|
+
writeAuthorized: writeStatus.authorized.map { $0.rawValue },
|
|
1198
|
+
writeDenied: writeStatus.denied.map { $0.rawValue }
|
|
1198
1199
|
)
|
|
1199
1200
|
completion(payload)
|
|
1200
1201
|
}
|
|
@@ -1223,40 +1224,60 @@ final class Health {
|
|
|
1223
1224
|
return (authorized, denied)
|
|
1224
1225
|
}
|
|
1225
1226
|
|
|
1226
|
-
private func readAuthorizationStatus(for types: [HealthDataType], completion: @escaping ([
|
|
1227
|
-
guard !types.isEmpty else {
|
|
1227
|
+
private func readAuthorizationStatus(for types: [HealthDataType], includeWorkouts: Bool, completion: @escaping ([String], [String]) -> Void) {
|
|
1228
|
+
guard !types.isEmpty || includeWorkouts else {
|
|
1228
1229
|
completion([], [])
|
|
1229
1230
|
return
|
|
1230
1231
|
}
|
|
1231
1232
|
|
|
1232
1233
|
let group = DispatchGroup()
|
|
1233
1234
|
let lock = NSLock()
|
|
1234
|
-
var authorized: [
|
|
1235
|
-
var denied: [
|
|
1235
|
+
var authorized: [String] = []
|
|
1236
|
+
var denied: [String] = []
|
|
1236
1237
|
|
|
1237
1238
|
for type in types {
|
|
1238
|
-
guard let
|
|
1239
|
-
denied.append(type)
|
|
1239
|
+
guard let readSet = try? readAuthorizationObjectTypes(for: type) else {
|
|
1240
|
+
denied.append(type.rawValue)
|
|
1240
1241
|
continue
|
|
1241
1242
|
}
|
|
1242
1243
|
|
|
1243
1244
|
group.enter()
|
|
1244
|
-
|
|
1245
|
-
|
|
1245
|
+
healthStore.getRequestStatusForAuthorization(toShare: emptyWriteTypesSet, read: readSet) { status, error in
|
|
1246
|
+
defer { group.leave() }
|
|
1247
|
+
|
|
1248
|
+
if error != nil {
|
|
1249
|
+
lock.lock(); denied.append(type.rawValue); lock.unlock()
|
|
1250
|
+
return
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
switch status {
|
|
1254
|
+
case .unnecessary:
|
|
1255
|
+
lock.lock(); authorized.append(type.rawValue); lock.unlock()
|
|
1256
|
+
case .shouldRequest, .unknown:
|
|
1257
|
+
lock.lock(); denied.append(type.rawValue); lock.unlock()
|
|
1258
|
+
@unknown default:
|
|
1259
|
+
lock.lock(); denied.append(type.rawValue); lock.unlock()
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
if includeWorkouts {
|
|
1265
|
+
group.enter()
|
|
1266
|
+
healthStore.getRequestStatusForAuthorization(toShare: emptyWriteTypesSet, read: [HKObjectType.workoutType()]) { status, error in
|
|
1246
1267
|
defer { group.leave() }
|
|
1247
1268
|
|
|
1248
1269
|
if error != nil {
|
|
1249
|
-
lock.lock(); denied.append(
|
|
1270
|
+
lock.lock(); denied.append("workouts"); lock.unlock()
|
|
1250
1271
|
return
|
|
1251
1272
|
}
|
|
1252
1273
|
|
|
1253
1274
|
switch status {
|
|
1254
1275
|
case .unnecessary:
|
|
1255
|
-
lock.lock(); authorized.append(
|
|
1276
|
+
lock.lock(); authorized.append("workouts"); lock.unlock()
|
|
1256
1277
|
case .shouldRequest, .unknown:
|
|
1257
|
-
lock.lock(); denied.append(
|
|
1278
|
+
lock.lock(); denied.append("workouts"); lock.unlock()
|
|
1258
1279
|
@unknown default:
|
|
1259
|
-
lock.lock(); denied.append(
|
|
1280
|
+
lock.lock(); denied.append("workouts"); lock.unlock()
|
|
1260
1281
|
}
|
|
1261
1282
|
}
|
|
1262
1283
|
}
|
|
@@ -1330,11 +1351,23 @@ final class Health {
|
|
|
1330
1351
|
}
|
|
1331
1352
|
}
|
|
1332
1353
|
|
|
1333
|
-
private func
|
|
1354
|
+
private func readAuthorizationObjectTypes(for dataType: HealthDataType) throws -> Set<HKObjectType> {
|
|
1355
|
+
if dataType == .bloodPressure {
|
|
1356
|
+
guard let systolicType = HKObjectType.quantityType(forIdentifier: .bloodPressureSystolic),
|
|
1357
|
+
let diastolicType = HKObjectType.quantityType(forIdentifier: .bloodPressureDiastolic) else {
|
|
1358
|
+
throw HealthManagerError.dataTypeUnavailable(dataType.rawValue)
|
|
1359
|
+
}
|
|
1360
|
+
return [systolicType, diastolicType]
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
return [try dataType.sampleType()]
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
private func readAuthorizationObjectTypes(for dataTypes: [HealthDataType]) throws -> Set<HKObjectType> {
|
|
1334
1367
|
var set = Set<HKObjectType>()
|
|
1335
1368
|
for dataType in dataTypes {
|
|
1336
|
-
let
|
|
1337
|
-
set.
|
|
1369
|
+
let types = try readAuthorizationObjectTypes(for: dataType)
|
|
1370
|
+
set.formUnion(types)
|
|
1338
1371
|
}
|
|
1339
1372
|
return set
|
|
1340
1373
|
}
|
|
@@ -3,7 +3,7 @@ import Capacitor
|
|
|
3
3
|
|
|
4
4
|
@objc(HealthPlugin)
|
|
5
5
|
public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
6
|
-
private let pluginVersion: String = "8.4.
|
|
6
|
+
private let pluginVersion: String = "8.4.6"
|
|
7
7
|
public let identifier = "HealthPlugin"
|
|
8
8
|
public let jsName = "Health"
|
|
9
9
|
public let pluginMethods: [CAPPluginMethod] = [
|
package/package.json
CHANGED