@capgo/capacitor-health 8.1.4 → 8.2.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
@@ -163,6 +163,7 @@ All write operations expect the default unit shown above. On Android the `metada
163
163
  * [`getPluginVersion()`](#getpluginversion)
164
164
  * [`openHealthConnectSettings()`](#openhealthconnectsettings)
165
165
  * [`showPrivacyPolicy()`](#showprivacypolicy)
166
+ * [`queryWorkouts(...)`](#queryworkouts)
166
167
  * [Interfaces](#interfaces)
167
168
  * [Type Aliases](#type-aliases)
168
169
 
@@ -297,6 +298,24 @@ or by placing an HTML file at www/privacypolicy.html in your assets.
297
298
  --------------------
298
299
 
299
300
 
301
+ ### queryWorkouts(...)
302
+
303
+ ```typescript
304
+ queryWorkouts(options: QueryWorkoutsOptions) => Promise<QueryWorkoutsResult>
305
+ ```
306
+
307
+ Queries workout sessions from the native health store.
308
+ Supported on iOS (HealthKit) and Android (Health Connect).
309
+
310
+ | Param | Type | Description |
311
+ | ------------- | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
312
+ | **`options`** | <code><a href="#queryworkoutsoptions">QueryWorkoutsOptions</a></code> | Query options including optional workout type filter, date range, limit, and sort order |
313
+
314
+ **Returns:** <code>Promise&lt;<a href="#queryworkoutsresult">QueryWorkoutsResult</a>&gt;</code>
315
+
316
+ --------------------
317
+
318
+
300
319
  ### Interfaces
301
320
 
302
321
 
@@ -370,6 +389,39 @@ or by placing an HTML file at www/privacypolicy.html in your assets.
370
389
  | **`metadata`** | <code><a href="#record">Record</a>&lt;string, string&gt;</code> | Metadata key-value pairs forwarded to the native APIs where supported. |
371
390
 
372
391
 
392
+ #### QueryWorkoutsResult
393
+
394
+ | Prop | Type |
395
+ | -------------- | ---------------------- |
396
+ | **`workouts`** | <code>Workout[]</code> |
397
+
398
+
399
+ #### Workout
400
+
401
+ | Prop | Type | Description |
402
+ | ----------------------- | --------------------------------------------------------------- | --------------------------------------------------- |
403
+ | **`workoutType`** | <code><a href="#workouttype">WorkoutType</a></code> | The type of workout. |
404
+ | **`duration`** | <code>number</code> | Duration of the workout in seconds. |
405
+ | **`totalEnergyBurned`** | <code>number</code> | Total energy burned in kilocalories (if available). |
406
+ | **`totalDistance`** | <code>number</code> | Total distance in meters (if available). |
407
+ | **`startDate`** | <code>string</code> | ISO 8601 start date of the workout. |
408
+ | **`endDate`** | <code>string</code> | ISO 8601 end date of the workout. |
409
+ | **`sourceName`** | <code>string</code> | Source name that recorded the workout. |
410
+ | **`sourceId`** | <code>string</code> | Source bundle identifier. |
411
+ | **`metadata`** | <code><a href="#record">Record</a>&lt;string, string&gt;</code> | Additional metadata (if available). |
412
+
413
+
414
+ #### QueryWorkoutsOptions
415
+
416
+ | Prop | Type | Description |
417
+ | ----------------- | --------------------------------------------------- | ------------------------------------------------------------------------- |
418
+ | **`workoutType`** | <code><a href="#workouttype">WorkoutType</a></code> | Optional workout type filter. If omitted, all workout types are returned. |
419
+ | **`startDate`** | <code>string</code> | Inclusive ISO 8601 start date (defaults to now - 1 day). |
420
+ | **`endDate`** | <code>string</code> | Exclusive ISO 8601 end date (defaults to now). |
421
+ | **`limit`** | <code>number</code> | Maximum number of workouts to return (defaults to 100). |
422
+ | **`ascending`** | <code>boolean</code> | Return results sorted ascending by start date (defaults to false). |
423
+
424
+
373
425
  ### Type Aliases
374
426
 
375
427
 
@@ -389,6 +441,11 @@ Construct a type with a set of properties K of type T
389
441
 
390
442
  <code>{
391
443
  [P in K]: T;
392
444
  }</code>
393
445
 
446
+
447
+ #### WorkoutType
448
+
449
+ <code>'running' | 'cycling' | 'walking' | 'swimming' | 'yoga' | 'strengthTraining' | 'hiking' | 'tennis' | 'basketball' | 'soccer' | 'americanFootball' | 'baseball' | 'crossTraining' | 'elliptical' | 'rowing' | 'stairClimbing' | 'traditionalStrengthTraining' | 'waterFitness' | 'waterPolo' | 'waterSports' | 'wrestling' | 'other'</code>
450
+
394
451
  </docgen-api>
395
452
 
396
453
  ### Credits:
@@ -1,8 +1,11 @@
1
1
  package app.capgo.plugin.health
2
2
 
3
3
  import androidx.health.connect.client.HealthConnectClient
4
+ import androidx.health.connect.client.permission.HealthPermission
5
+ import androidx.health.connect.client.request.AggregateRequest
4
6
  import androidx.health.connect.client.records.ActiveCaloriesBurnedRecord
5
7
  import androidx.health.connect.client.records.DistanceRecord
8
+ import androidx.health.connect.client.records.ExerciseSessionRecord
6
9
  import androidx.health.connect.client.records.HeartRateRecord
7
10
  import androidx.health.connect.client.records.Record
8
11
  import androidx.health.connect.client.records.StepsRecord
@@ -13,6 +16,7 @@ import androidx.health.connect.client.units.Energy
13
16
  import androidx.health.connect.client.units.Length
14
17
  import androidx.health.connect.client.units.Mass
15
18
  import androidx.health.connect.client.records.metadata.Metadata
19
+ import java.time.Duration
16
20
  import com.getcapacitor.JSArray
17
21
  import com.getcapacitor.JSObject
18
22
  import java.time.Instant
@@ -26,15 +30,20 @@ class HealthManager {
26
30
 
27
31
  private val formatter: DateTimeFormatter = DateTimeFormatter.ISO_INSTANT
28
32
 
29
- fun permissionsFor(readTypes: Collection<HealthDataType>, writeTypes: Collection<HealthDataType>): Set<String> = buildSet {
33
+ fun permissionsFor(readTypes: Collection<HealthDataType>, writeTypes: Collection<HealthDataType>, includeWorkouts: Boolean = false): Set<String> = buildSet {
30
34
  readTypes.forEach { add(it.readPermission) }
31
35
  writeTypes.forEach { add(it.writePermission) }
36
+ // Include workout read permission if explicitly requested
37
+ if (includeWorkouts) {
38
+ add(HealthPermission.getReadPermission(ExerciseSessionRecord::class))
39
+ }
32
40
  }
33
41
 
34
42
  suspend fun authorizationStatus(
35
43
  client: HealthConnectClient,
36
44
  readTypes: Collection<HealthDataType>,
37
- writeTypes: Collection<HealthDataType>
45
+ writeTypes: Collection<HealthDataType>,
46
+ includeWorkouts: Boolean = false
38
47
  ): JSObject {
39
48
  val granted = client.permissionController.getGrantedPermissions()
40
49
 
@@ -48,6 +57,16 @@ class HealthManager {
48
57
  }
49
58
  }
50
59
 
60
+ // Check workout permission if requested
61
+ if (includeWorkouts) {
62
+ val workoutPermission = HealthPermission.getReadPermission(ExerciseSessionRecord::class)
63
+ if (granted.contains(workoutPermission)) {
64
+ readAuthorized.put("workouts")
65
+ } else {
66
+ readDenied.put("workouts")
67
+ }
68
+ }
69
+
51
70
  val writeAuthorized = JSArray()
52
71
  val writeDenied = JSArray()
53
72
  writeTypes.forEach { type ->
@@ -273,6 +292,127 @@ class HealthManager {
273
292
  return java.lang.Math.round(this.coerceAtLeast(0.0))
274
293
  }
275
294
 
295
+ suspend fun queryWorkouts(
296
+ client: HealthConnectClient,
297
+ workoutType: String?,
298
+ startTime: Instant,
299
+ endTime: Instant,
300
+ limit: Int,
301
+ ascending: Boolean
302
+ ): JSArray {
303
+ val workouts = mutableListOf<Pair<Instant, JSObject>>()
304
+
305
+ var pageToken: String? = null
306
+ val pageSize = if (limit > 0) min(limit, MAX_PAGE_SIZE) else DEFAULT_PAGE_SIZE
307
+ var fetched = 0
308
+
309
+ val exerciseTypeFilter = WorkoutType.fromString(workoutType)
310
+
311
+ do {
312
+ val request = ReadRecordsRequest(
313
+ recordType = ExerciseSessionRecord::class,
314
+ timeRangeFilter = TimeRangeFilter.between(startTime, endTime),
315
+ pageSize = pageSize,
316
+ pageToken = pageToken
317
+ )
318
+ val response = client.readRecords(request)
319
+
320
+ response.records.forEach { record ->
321
+ val session = record as ExerciseSessionRecord
322
+
323
+ // Filter by exercise type if specified
324
+ if (exerciseTypeFilter != null && session.exerciseType != exerciseTypeFilter) {
325
+ return@forEach
326
+ }
327
+
328
+ // Aggregate calories and distance for this workout session
329
+ val aggregatedData = aggregateWorkoutData(client, session)
330
+ val payload = createWorkoutPayload(session, aggregatedData)
331
+ workouts.add(session.startTime to payload)
332
+ }
333
+
334
+ fetched += response.records.size
335
+ pageToken = response.pageToken
336
+ } while (pageToken != null && (limit <= 0 || fetched < limit))
337
+
338
+ val sorted = workouts.sortedBy { it.first }
339
+ val ordered = if (ascending) sorted else sorted.asReversed()
340
+ val limited = if (limit > 0) ordered.take(limit) else ordered
341
+
342
+ val array = JSArray()
343
+ limited.forEach { array.put(it.second) }
344
+ return array
345
+ }
346
+
347
+ private suspend fun aggregateWorkoutData(
348
+ client: HealthConnectClient,
349
+ session: ExerciseSessionRecord
350
+ ): WorkoutAggregatedData {
351
+ val timeRange = TimeRangeFilter.between(session.startTime, session.endTime)
352
+ // Don't filter by dataOrigin - distance might come from different sources
353
+ // than the workout session itself (e.g., fitness tracker vs workout app)
354
+
355
+ // Aggregate distance
356
+ val distanceAggregate = try {
357
+ val aggregateRequest = AggregateRequest(
358
+ metrics = setOf(DistanceRecord.DISTANCE_TOTAL),
359
+ timeRangeFilter = timeRange
360
+ // Removed dataOriginFilter to get distance from all sources during workout time
361
+ )
362
+ val result = client.aggregate(aggregateRequest)
363
+ result[DistanceRecord.DISTANCE_TOTAL]?.inMeters
364
+ } catch (e: Exception) {
365
+ android.util.Log.d("HealthManager", "Distance aggregation failed for workout: ${e.message}", e)
366
+ null // Permission might not be granted or no data available
367
+ }
368
+
369
+ return WorkoutAggregatedData(
370
+ totalDistance = distanceAggregate
371
+ )
372
+ }
373
+
374
+ private data class WorkoutAggregatedData(
375
+ val totalDistance: Double?
376
+ )
377
+
378
+ private fun createWorkoutPayload(session: ExerciseSessionRecord, aggregatedData: WorkoutAggregatedData): JSObject {
379
+ val payload = JSObject()
380
+
381
+ // Workout type
382
+ payload.put("workoutType", WorkoutType.toWorkoutTypeString(session.exerciseType))
383
+
384
+ // Duration in seconds
385
+ val durationSeconds = Duration.between(session.startTime, session.endTime).seconds.toInt()
386
+ payload.put("duration", durationSeconds)
387
+
388
+ // Start and end dates
389
+ payload.put("startDate", formatter.format(session.startTime))
390
+ payload.put("endDate", formatter.format(session.endTime))
391
+
392
+ // Total distance (aggregated from DistanceRecord)
393
+ aggregatedData.totalDistance?.let { distance ->
394
+ payload.put("totalDistance", distance)
395
+ }
396
+
397
+ // Source information
398
+ val dataOrigin = session.metadata.dataOrigin
399
+ payload.put("sourceId", dataOrigin.packageName)
400
+ payload.put("sourceName", dataOrigin.packageName)
401
+ session.metadata.device?.let { device ->
402
+ val manufacturer = device.manufacturer?.takeIf { it.isNotBlank() }
403
+ val model = device.model?.takeIf { it.isNotBlank() }
404
+ val label = listOfNotNull(manufacturer, model).joinToString(" ").trim()
405
+ if (label.isNotEmpty()) {
406
+ payload.put("sourceName", label)
407
+ }
408
+ }
409
+
410
+ // Note: customMetadata is not available on Metadata in Health Connect
411
+ // Metadata only contains dataOrigin, device, and lastModifiedTime
412
+
413
+ return payload
414
+ }
415
+
276
416
  companion object {
277
417
  private const val DEFAULT_PAGE_SIZE = 100
278
418
  private const val MAX_PAGE_SIZE = 500
@@ -32,6 +32,7 @@ class HealthPlugin : Plugin() {
32
32
  // Store pending request data for callback
33
33
  private var pendingReadTypes: List<HealthDataType> = emptyList()
34
34
  private var pendingWriteTypes: List<HealthDataType> = emptyList()
35
+ private var pendingIncludeWorkouts: Boolean = false
35
36
 
36
37
  override fun handleOnDestroy() {
37
38
  super.handleOnDestroy()
@@ -46,8 +47,8 @@ class HealthPlugin : Plugin() {
46
47
 
47
48
  @PluginMethod
48
49
  fun requestAuthorization(call: PluginCall) {
49
- val readTypes = try {
50
- parseTypeList(call, "read")
50
+ val (readTypes, includeWorkouts) = try {
51
+ parseTypeListWithWorkouts(call, "read")
51
52
  } catch (e: IllegalArgumentException) {
52
53
  call.reject(e.message, null, e)
53
54
  return
@@ -62,17 +63,17 @@ class HealthPlugin : Plugin() {
62
63
 
63
64
  pluginScope.launch {
64
65
  val client = getClientOrReject(call) ?: return@launch
65
- val permissions = manager.permissionsFor(readTypes, writeTypes)
66
+ val permissions = manager.permissionsFor(readTypes, writeTypes, includeWorkouts)
66
67
 
67
68
  if (permissions.isEmpty()) {
68
- val status = manager.authorizationStatus(client, readTypes, writeTypes)
69
+ val status = manager.authorizationStatus(client, readTypes, writeTypes, includeWorkouts)
69
70
  call.resolve(status)
70
71
  return@launch
71
72
  }
72
73
 
73
74
  val granted = client.permissionController.getGrantedPermissions()
74
75
  if (granted.containsAll(permissions)) {
75
- val status = manager.authorizationStatus(client, readTypes, writeTypes)
76
+ val status = manager.authorizationStatus(client, readTypes, writeTypes, includeWorkouts)
76
77
  call.resolve(status)
77
78
  return@launch
78
79
  }
@@ -80,6 +81,7 @@ class HealthPlugin : Plugin() {
80
81
  // Store types for callback
81
82
  pendingReadTypes = readTypes
82
83
  pendingWriteTypes = writeTypes
84
+ pendingIncludeWorkouts = includeWorkouts
83
85
 
84
86
  // Create intent using the Health Connect permission contract
85
87
  val intent = permissionContract.createIntent(context, permissions)
@@ -102,20 +104,22 @@ class HealthPlugin : Plugin() {
102
104
 
103
105
  val readTypes = pendingReadTypes
104
106
  val writeTypes = pendingWriteTypes
107
+ val includeWorkouts = pendingIncludeWorkouts
105
108
  pendingReadTypes = emptyList()
106
109
  pendingWriteTypes = emptyList()
110
+ pendingIncludeWorkouts = false
107
111
 
108
112
  pluginScope.launch {
109
113
  val client = getClientOrReject(call) ?: return@launch
110
- val status = manager.authorizationStatus(client, readTypes, writeTypes)
114
+ val status = manager.authorizationStatus(client, readTypes, writeTypes, includeWorkouts)
111
115
  call.resolve(status)
112
116
  }
113
117
  }
114
118
 
115
119
  @PluginMethod
116
120
  fun checkAuthorization(call: PluginCall) {
117
- val readTypes = try {
118
- parseTypeList(call, "read")
121
+ val (readTypes, includeWorkouts) = try {
122
+ parseTypeListWithWorkouts(call, "read")
119
123
  } catch (e: IllegalArgumentException) {
120
124
  call.reject(e.message, null, e)
121
125
  return
@@ -130,7 +134,7 @@ class HealthPlugin : Plugin() {
130
134
 
131
135
  pluginScope.launch {
132
136
  val client = getClientOrReject(call) ?: return@launch
133
- val status = manager.authorizationStatus(client, readTypes, writeTypes)
137
+ val status = manager.authorizationStatus(client, readTypes, writeTypes, includeWorkouts)
134
138
  call.resolve(status)
135
139
  }
136
140
  }
@@ -265,6 +269,23 @@ class HealthPlugin : Plugin() {
265
269
  return result
266
270
  }
267
271
 
272
+ private fun parseTypeListWithWorkouts(call: PluginCall, key: String): Pair<List<HealthDataType>, Boolean> {
273
+ val array = call.getArray(key) ?: JSArray()
274
+ val result = mutableListOf<HealthDataType>()
275
+ var includeWorkouts = false
276
+ for (i in 0 until array.length()) {
277
+ val identifier = array.optString(i, null) ?: continue
278
+ if (identifier == "workouts") {
279
+ includeWorkouts = true
280
+ } else {
281
+ val dataType = HealthDataType.from(identifier)
282
+ ?: throw IllegalArgumentException("Unsupported data type: $identifier")
283
+ result.add(dataType)
284
+ }
285
+ }
286
+ return Pair(result, includeWorkouts)
287
+ }
288
+
268
289
  private fun getClientOrReject(call: PluginCall): HealthConnectClient? {
269
290
  val status = HealthConnectClient.getSdkStatus(context)
270
291
  if (status != HealthConnectClient.SDK_AVAILABLE) {
@@ -327,6 +348,43 @@ class HealthPlugin : Plugin() {
327
348
  }
328
349
  }
329
350
 
351
+ @PluginMethod
352
+ fun queryWorkouts(call: PluginCall) {
353
+ val workoutType = call.getString("workoutType")
354
+ val limit = (call.getInt("limit") ?: DEFAULT_LIMIT).coerceAtLeast(0)
355
+ val ascending = call.getBoolean("ascending") ?: false
356
+
357
+ val startInstant = try {
358
+ manager.parseInstant(call.getString("startDate"), Instant.now().minus(DEFAULT_PAST_DURATION))
359
+ } catch (e: DateTimeParseException) {
360
+ call.reject(e.message, null, e)
361
+ return
362
+ }
363
+
364
+ val endInstant = try {
365
+ manager.parseInstant(call.getString("endDate"), Instant.now())
366
+ } catch (e: DateTimeParseException) {
367
+ call.reject(e.message, null, e)
368
+ return
369
+ }
370
+
371
+ if (endInstant.isBefore(startInstant)) {
372
+ call.reject("endDate must be greater than or equal to startDate")
373
+ return
374
+ }
375
+
376
+ pluginScope.launch {
377
+ val client = getClientOrReject(call) ?: return@launch
378
+ try {
379
+ val workouts = manager.queryWorkouts(client, workoutType, startInstant, endInstant, limit, ascending)
380
+ val result = JSObject().apply { put("workouts", workouts) }
381
+ call.resolve(result)
382
+ } catch (e: Exception) {
383
+ call.reject(e.message ?: "Failed to query workouts.", null, e)
384
+ }
385
+ }
386
+ }
387
+
330
388
  companion object {
331
389
  private const val DEFAULT_LIMIT = 100
332
390
  private val DEFAULT_PAST_DURATION: Duration = Duration.ofDays(1)
@@ -0,0 +1,64 @@
1
+ package app.capgo.plugin.health
2
+
3
+ import androidx.health.connect.client.records.ExerciseSessionRecord
4
+
5
+ object WorkoutType {
6
+ fun fromString(type: String?): Int? {
7
+ if (type.isNullOrBlank()) return null
8
+
9
+ return when (type) {
10
+ "running" -> ExerciseSessionRecord.EXERCISE_TYPE_RUNNING
11
+ "cycling" -> ExerciseSessionRecord.EXERCISE_TYPE_BIKING
12
+ "walking" -> ExerciseSessionRecord.EXERCISE_TYPE_WALKING
13
+ "swimming" -> ExerciseSessionRecord.EXERCISE_TYPE_SWIMMING_POOL
14
+ "yoga" -> ExerciseSessionRecord.EXERCISE_TYPE_YOGA
15
+ "strengthTraining" -> ExerciseSessionRecord.EXERCISE_TYPE_STRENGTH_TRAINING
16
+ "hiking" -> ExerciseSessionRecord.EXERCISE_TYPE_HIKING
17
+ "tennis" -> ExerciseSessionRecord.EXERCISE_TYPE_TENNIS
18
+ "basketball" -> ExerciseSessionRecord.EXERCISE_TYPE_BASKETBALL
19
+ "soccer" -> ExerciseSessionRecord.EXERCISE_TYPE_SOCCER
20
+ "americanFootball" -> ExerciseSessionRecord.EXERCISE_TYPE_FOOTBALL_AMERICAN
21
+ "baseball" -> ExerciseSessionRecord.EXERCISE_TYPE_BASEBALL
22
+ "crossTraining" -> ExerciseSessionRecord.EXERCISE_TYPE_HIGH_INTENSITY_INTERVAL_TRAINING
23
+ "elliptical" -> ExerciseSessionRecord.EXERCISE_TYPE_ELLIPTICAL
24
+ "rowing" -> ExerciseSessionRecord.EXERCISE_TYPE_ROWING
25
+ "stairClimbing" -> ExerciseSessionRecord.EXERCISE_TYPE_STAIR_CLIMBING
26
+ "traditionalStrengthTraining" -> ExerciseSessionRecord.EXERCISE_TYPE_STRENGTH_TRAINING
27
+ "waterFitness" -> ExerciseSessionRecord.EXERCISE_TYPE_SWIMMING_POOL
28
+ "waterPolo" -> ExerciseSessionRecord.EXERCISE_TYPE_WATER_POLO
29
+ "waterSports" -> ExerciseSessionRecord.EXERCISE_TYPE_SWIMMING_OPEN_WATER
30
+ "wrestling" -> ExerciseSessionRecord.EXERCISE_TYPE_MARTIAL_ARTS
31
+ "other" -> ExerciseSessionRecord.EXERCISE_TYPE_OTHER_WORKOUT
32
+ else -> null
33
+ }
34
+ }
35
+
36
+ fun toWorkoutTypeString(exerciseType: Int): String {
37
+ return when (exerciseType) {
38
+ ExerciseSessionRecord.EXERCISE_TYPE_RUNNING -> "running"
39
+ ExerciseSessionRecord.EXERCISE_TYPE_BIKING -> "cycling"
40
+ ExerciseSessionRecord.EXERCISE_TYPE_BIKING_STATIONARY -> "cycling"
41
+ ExerciseSessionRecord.EXERCISE_TYPE_WALKING -> "walking"
42
+ ExerciseSessionRecord.EXERCISE_TYPE_SWIMMING_POOL -> "swimming"
43
+ ExerciseSessionRecord.EXERCISE_TYPE_SWIMMING_OPEN_WATER -> "swimming"
44
+ ExerciseSessionRecord.EXERCISE_TYPE_YOGA -> "yoga"
45
+ ExerciseSessionRecord.EXERCISE_TYPE_STRENGTH_TRAINING -> "strengthTraining"
46
+ ExerciseSessionRecord.EXERCISE_TYPE_HIKING -> "hiking"
47
+ ExerciseSessionRecord.EXERCISE_TYPE_TENNIS -> "tennis"
48
+ ExerciseSessionRecord.EXERCISE_TYPE_BASKETBALL -> "basketball"
49
+ ExerciseSessionRecord.EXERCISE_TYPE_SOCCER -> "soccer"
50
+ ExerciseSessionRecord.EXERCISE_TYPE_FOOTBALL_AMERICAN -> "americanFootball"
51
+ ExerciseSessionRecord.EXERCISE_TYPE_BASEBALL -> "baseball"
52
+ ExerciseSessionRecord.EXERCISE_TYPE_HIGH_INTENSITY_INTERVAL_TRAINING -> "crossTraining"
53
+ ExerciseSessionRecord.EXERCISE_TYPE_ELLIPTICAL -> "elliptical"
54
+ ExerciseSessionRecord.EXERCISE_TYPE_ROWING -> "rowing"
55
+ ExerciseSessionRecord.EXERCISE_TYPE_ROWING_MACHINE -> "rowing"
56
+ ExerciseSessionRecord.EXERCISE_TYPE_STAIR_CLIMBING -> "stairClimbing"
57
+ ExerciseSessionRecord.EXERCISE_TYPE_STAIR_CLIMBING_MACHINE -> "stairClimbing"
58
+ ExerciseSessionRecord.EXERCISE_TYPE_WATER_POLO -> "waterPolo"
59
+ ExerciseSessionRecord.EXERCISE_TYPE_MARTIAL_ARTS -> "wrestling"
60
+ ExerciseSessionRecord.EXERCISE_TYPE_OTHER_WORKOUT -> "other"
61
+ else -> "other"
62
+ }
63
+ }
64
+ }
package/dist/docs.json CHANGED
@@ -140,6 +140,38 @@
140
140
  "docs": "Shows the app's privacy policy for Health Connect (Android only).\nOn iOS, this method does nothing.\n\nThis displays the same privacy policy screen that Health Connect shows\nwhen the user taps \"Privacy policy\" in the permissions dialog.\n\nThe privacy policy URL can be configured by adding a string resource\nnamed \"health_connect_privacy_policy_url\" in your app's strings.xml,\nor by placing an HTML file at www/privacypolicy.html in your assets.",
141
141
  "complexTypes": [],
142
142
  "slug": "showprivacypolicy"
143
+ },
144
+ {
145
+ "name": "queryWorkouts",
146
+ "signature": "(options: QueryWorkoutsOptions) => Promise<QueryWorkoutsResult>",
147
+ "parameters": [
148
+ {
149
+ "name": "options",
150
+ "docs": "Query options including optional workout type filter, date range, limit, and sort order",
151
+ "type": "QueryWorkoutsOptions"
152
+ }
153
+ ],
154
+ "returns": "Promise<QueryWorkoutsResult>",
155
+ "tags": [
156
+ {
157
+ "name": "param",
158
+ "text": "options Query options including optional workout type filter, date range, limit, and sort order"
159
+ },
160
+ {
161
+ "name": "returns",
162
+ "text": "A promise that resolves with the workout sessions"
163
+ },
164
+ {
165
+ "name": "throws",
166
+ "text": "An error if something went wrong"
167
+ }
168
+ ],
169
+ "docs": "Queries workout sessions from the native health store.\nSupported on iOS (HealthKit) and Android (Health Connect).",
170
+ "complexTypes": [
171
+ "QueryWorkoutsResult",
172
+ "QueryWorkoutsOptions"
173
+ ],
174
+ "slug": "queryworkouts"
143
175
  }
144
176
  ],
145
177
  "properties": []
@@ -429,6 +461,146 @@
429
461
  "type": "Record<string, string>"
430
462
  }
431
463
  ]
464
+ },
465
+ {
466
+ "name": "QueryWorkoutsResult",
467
+ "slug": "queryworkoutsresult",
468
+ "docs": "",
469
+ "tags": [],
470
+ "methods": [],
471
+ "properties": [
472
+ {
473
+ "name": "workouts",
474
+ "tags": [],
475
+ "docs": "",
476
+ "complexTypes": [
477
+ "Workout"
478
+ ],
479
+ "type": "Workout[]"
480
+ }
481
+ ]
482
+ },
483
+ {
484
+ "name": "Workout",
485
+ "slug": "workout",
486
+ "docs": "",
487
+ "tags": [],
488
+ "methods": [],
489
+ "properties": [
490
+ {
491
+ "name": "workoutType",
492
+ "tags": [],
493
+ "docs": "The type of workout.",
494
+ "complexTypes": [
495
+ "WorkoutType"
496
+ ],
497
+ "type": "WorkoutType"
498
+ },
499
+ {
500
+ "name": "duration",
501
+ "tags": [],
502
+ "docs": "Duration of the workout in seconds.",
503
+ "complexTypes": [],
504
+ "type": "number"
505
+ },
506
+ {
507
+ "name": "totalEnergyBurned",
508
+ "tags": [],
509
+ "docs": "Total energy burned in kilocalories (if available).",
510
+ "complexTypes": [],
511
+ "type": "number | undefined"
512
+ },
513
+ {
514
+ "name": "totalDistance",
515
+ "tags": [],
516
+ "docs": "Total distance in meters (if available).",
517
+ "complexTypes": [],
518
+ "type": "number | undefined"
519
+ },
520
+ {
521
+ "name": "startDate",
522
+ "tags": [],
523
+ "docs": "ISO 8601 start date of the workout.",
524
+ "complexTypes": [],
525
+ "type": "string"
526
+ },
527
+ {
528
+ "name": "endDate",
529
+ "tags": [],
530
+ "docs": "ISO 8601 end date of the workout.",
531
+ "complexTypes": [],
532
+ "type": "string"
533
+ },
534
+ {
535
+ "name": "sourceName",
536
+ "tags": [],
537
+ "docs": "Source name that recorded the workout.",
538
+ "complexTypes": [],
539
+ "type": "string | undefined"
540
+ },
541
+ {
542
+ "name": "sourceId",
543
+ "tags": [],
544
+ "docs": "Source bundle identifier.",
545
+ "complexTypes": [],
546
+ "type": "string | undefined"
547
+ },
548
+ {
549
+ "name": "metadata",
550
+ "tags": [],
551
+ "docs": "Additional metadata (if available).",
552
+ "complexTypes": [
553
+ "Record"
554
+ ],
555
+ "type": "Record<string, string>"
556
+ }
557
+ ]
558
+ },
559
+ {
560
+ "name": "QueryWorkoutsOptions",
561
+ "slug": "queryworkoutsoptions",
562
+ "docs": "",
563
+ "tags": [],
564
+ "methods": [],
565
+ "properties": [
566
+ {
567
+ "name": "workoutType",
568
+ "tags": [],
569
+ "docs": "Optional workout type filter. If omitted, all workout types are returned.",
570
+ "complexTypes": [
571
+ "WorkoutType"
572
+ ],
573
+ "type": "WorkoutType"
574
+ },
575
+ {
576
+ "name": "startDate",
577
+ "tags": [],
578
+ "docs": "Inclusive ISO 8601 start date (defaults to now - 1 day).",
579
+ "complexTypes": [],
580
+ "type": "string | undefined"
581
+ },
582
+ {
583
+ "name": "endDate",
584
+ "tags": [],
585
+ "docs": "Exclusive ISO 8601 end date (defaults to now).",
586
+ "complexTypes": [],
587
+ "type": "string | undefined"
588
+ },
589
+ {
590
+ "name": "limit",
591
+ "tags": [],
592
+ "docs": "Maximum number of workouts to return (defaults to 100).",
593
+ "complexTypes": [],
594
+ "type": "number | undefined"
595
+ },
596
+ {
597
+ "name": "ascending",
598
+ "tags": [],
599
+ "docs": "Return results sorted ascending by start date (defaults to false).",
600
+ "complexTypes": [],
601
+ "type": "boolean | undefined"
602
+ }
603
+ ]
432
604
  }
433
605
  ],
434
606
  "enums": [],
@@ -500,6 +672,101 @@
500
672
  ]
501
673
  }
502
674
  ]
675
+ },
676
+ {
677
+ "name": "WorkoutType",
678
+ "slug": "workouttype",
679
+ "docs": "",
680
+ "types": [
681
+ {
682
+ "text": "'running'",
683
+ "complexTypes": []
684
+ },
685
+ {
686
+ "text": "'cycling'",
687
+ "complexTypes": []
688
+ },
689
+ {
690
+ "text": "'walking'",
691
+ "complexTypes": []
692
+ },
693
+ {
694
+ "text": "'swimming'",
695
+ "complexTypes": []
696
+ },
697
+ {
698
+ "text": "'yoga'",
699
+ "complexTypes": []
700
+ },
701
+ {
702
+ "text": "'strengthTraining'",
703
+ "complexTypes": []
704
+ },
705
+ {
706
+ "text": "'hiking'",
707
+ "complexTypes": []
708
+ },
709
+ {
710
+ "text": "'tennis'",
711
+ "complexTypes": []
712
+ },
713
+ {
714
+ "text": "'basketball'",
715
+ "complexTypes": []
716
+ },
717
+ {
718
+ "text": "'soccer'",
719
+ "complexTypes": []
720
+ },
721
+ {
722
+ "text": "'americanFootball'",
723
+ "complexTypes": []
724
+ },
725
+ {
726
+ "text": "'baseball'",
727
+ "complexTypes": []
728
+ },
729
+ {
730
+ "text": "'crossTraining'",
731
+ "complexTypes": []
732
+ },
733
+ {
734
+ "text": "'elliptical'",
735
+ "complexTypes": []
736
+ },
737
+ {
738
+ "text": "'rowing'",
739
+ "complexTypes": []
740
+ },
741
+ {
742
+ "text": "'stairClimbing'",
743
+ "complexTypes": []
744
+ },
745
+ {
746
+ "text": "'traditionalStrengthTraining'",
747
+ "complexTypes": []
748
+ },
749
+ {
750
+ "text": "'waterFitness'",
751
+ "complexTypes": []
752
+ },
753
+ {
754
+ "text": "'waterPolo'",
755
+ "complexTypes": []
756
+ },
757
+ {
758
+ "text": "'waterSports'",
759
+ "complexTypes": []
760
+ },
761
+ {
762
+ "text": "'wrestling'",
763
+ "complexTypes": []
764
+ },
765
+ {
766
+ "text": "'other'",
767
+ "complexTypes": []
768
+ }
769
+ ]
503
770
  }
504
771
  ],
505
772
  "pluginConfigs": []
@@ -42,6 +42,42 @@ export interface HealthSample {
42
42
  export interface ReadSamplesResult {
43
43
  samples: HealthSample[];
44
44
  }
45
+ export type WorkoutType = 'running' | 'cycling' | 'walking' | 'swimming' | 'yoga' | 'strengthTraining' | 'hiking' | 'tennis' | 'basketball' | 'soccer' | 'americanFootball' | 'baseball' | 'crossTraining' | 'elliptical' | 'rowing' | 'stairClimbing' | 'traditionalStrengthTraining' | 'waterFitness' | 'waterPolo' | 'waterSports' | 'wrestling' | 'other';
46
+ export interface QueryWorkoutsOptions {
47
+ /** Optional workout type filter. If omitted, all workout types are returned. */
48
+ workoutType?: WorkoutType;
49
+ /** Inclusive ISO 8601 start date (defaults to now - 1 day). */
50
+ startDate?: string;
51
+ /** Exclusive ISO 8601 end date (defaults to now). */
52
+ endDate?: string;
53
+ /** Maximum number of workouts to return (defaults to 100). */
54
+ limit?: number;
55
+ /** Return results sorted ascending by start date (defaults to false). */
56
+ ascending?: boolean;
57
+ }
58
+ export interface Workout {
59
+ /** The type of workout. */
60
+ workoutType: WorkoutType;
61
+ /** Duration of the workout in seconds. */
62
+ duration: number;
63
+ /** Total energy burned in kilocalories (if available). */
64
+ totalEnergyBurned?: number;
65
+ /** Total distance in meters (if available). */
66
+ totalDistance?: number;
67
+ /** ISO 8601 start date of the workout. */
68
+ startDate: string;
69
+ /** ISO 8601 end date of the workout. */
70
+ endDate: string;
71
+ /** Source name that recorded the workout. */
72
+ sourceName?: string;
73
+ /** Source bundle identifier. */
74
+ sourceId?: string;
75
+ /** Additional metadata (if available). */
76
+ metadata?: Record<string, string>;
77
+ }
78
+ export interface QueryWorkoutsResult {
79
+ workouts: Workout[];
80
+ }
45
81
  export interface WriteSampleOptions {
46
82
  dataType: HealthDataType;
47
83
  value: number;
@@ -101,4 +137,13 @@ export interface HealthPlugin {
101
137
  * @throws An error if the privacy policy cannot be displayed
102
138
  */
103
139
  showPrivacyPolicy(): Promise<void>;
140
+ /**
141
+ * Queries workout sessions from the native health store.
142
+ * Supported on iOS (HealthKit) and Android (Health Connect).
143
+ *
144
+ * @param options Query options including optional workout type filter, date range, limit, and sort order
145
+ * @returns A promise that resolves with the workout sessions
146
+ * @throws An error if something went wrong
147
+ */
148
+ queryWorkouts(options: QueryWorkoutsOptions): Promise<QueryWorkoutsResult>;
104
149
  }
@@ -1 +1 @@
1
- {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["export type HealthDataType = 'steps' | 'distance' | 'calories' | 'heartRate' | 'weight';\n\nexport type HealthUnit = 'count' | 'meter' | 'kilocalorie' | 'bpm' | 'kilogram';\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 interface HealthSample {\n dataType: HealthDataType;\n value: number;\n unit: HealthUnit;\n startDate: string;\n endDate: string;\n sourceName?: string;\n sourceId?: string;\n}\n\nexport interface ReadSamplesResult {\n samples: HealthSample[];\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}\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"]}
1
+ {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["export type HealthDataType = 'steps' | 'distance' | 'calories' | 'heartRate' | 'weight';\n\nexport type HealthUnit = 'count' | 'meter' | 'kilocalorie' | 'bpm' | 'kilogram';\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 interface HealthSample {\n dataType: HealthDataType;\n value: number;\n unit: HealthUnit;\n startDate: string;\n endDate: string;\n sourceName?: string;\n sourceId?: string;\n}\n\nexport interface ReadSamplesResult {\n samples: HealthSample[];\n}\n\nexport type WorkoutType =\n | 'running'\n | 'cycling'\n | 'walking'\n | 'swimming'\n | 'yoga'\n | 'strengthTraining'\n | 'hiking'\n | 'tennis'\n | 'basketball'\n | 'soccer'\n | 'americanFootball'\n | 'baseball'\n | 'crossTraining'\n | 'elliptical'\n | 'rowing'\n | 'stairClimbing'\n | 'traditionalStrengthTraining'\n | 'waterFitness'\n | 'waterPolo'\n | 'waterSports'\n | 'wrestling'\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\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 /** Additional metadata (if available). */\n metadata?: Record<string, string>;\n}\n\nexport interface QueryWorkoutsResult {\n workouts: Workout[];\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}\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"]}
package/dist/esm/web.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { WebPlugin } from '@capacitor/core';
2
- import type { AuthorizationOptions, AuthorizationStatus, AvailabilityResult, HealthPlugin, QueryOptions, ReadSamplesResult, WriteSampleOptions } from './definitions';
2
+ import type { AuthorizationOptions, AuthorizationStatus, AvailabilityResult, HealthPlugin, QueryOptions, QueryWorkoutsOptions, QueryWorkoutsResult, ReadSamplesResult, WriteSampleOptions } from './definitions';
3
3
  export declare class HealthWeb extends WebPlugin implements HealthPlugin {
4
4
  isAvailable(): Promise<AvailabilityResult>;
5
5
  requestAuthorization(_options: AuthorizationOptions): Promise<AuthorizationStatus>;
@@ -11,4 +11,5 @@ export declare class HealthWeb extends WebPlugin implements HealthPlugin {
11
11
  }>;
12
12
  openHealthConnectSettings(): Promise<void>;
13
13
  showPrivacyPolicy(): Promise<void>;
14
+ queryWorkouts(_options: QueryWorkoutsOptions): Promise<QueryWorkoutsResult>;
14
15
  }
package/dist/esm/web.js CHANGED
@@ -28,5 +28,8 @@ export class HealthWeb extends WebPlugin {
28
28
  async showPrivacyPolicy() {
29
29
  // No-op on web - Health Connect privacy policy is Android only
30
30
  }
31
+ async queryWorkouts(_options) {
32
+ throw this.unimplemented('Querying workouts is only available on native platforms.');
33
+ }
31
34
  }
32
35
  //# sourceMappingURL=web.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"web.js","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAY5C,MAAM,OAAO,SAAU,SAAQ,SAAS;IACtC,KAAK,CAAC,WAAW;QACf,OAAO;YACL,SAAS,EAAE,KAAK;YAChB,QAAQ,EAAE,KAAK;YACf,MAAM,EAAE,iEAAiE;SAC1E,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,oBAAoB,CAAC,QAA8B;QACvD,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC,CAAC;IACzF,CAAC;IAED,KAAK,CAAC,kBAAkB,CAAC,QAA8B;QACrD,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC,CAAC;IACzF,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,QAAsB;QACtC,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC,CAAC;IACzF,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,QAA4B;QAC3C,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC,CAAC;IACzF,CAAC;IAED,KAAK,CAAC,gBAAgB;QACpB,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC5B,CAAC;IAED,KAAK,CAAC,yBAAyB;QAC7B,gDAAgD;IAClD,CAAC;IAED,KAAK,CAAC,iBAAiB;QACrB,+DAA+D;IACjE,CAAC;CACF","sourcesContent":["import { WebPlugin } from '@capacitor/core';\n\nimport type {\n AuthorizationOptions,\n AuthorizationStatus,\n AvailabilityResult,\n HealthPlugin,\n QueryOptions,\n ReadSamplesResult,\n WriteSampleOptions,\n} from './definitions';\n\nexport class HealthWeb extends WebPlugin implements HealthPlugin {\n async isAvailable(): Promise<AvailabilityResult> {\n return {\n available: false,\n platform: 'web',\n reason: 'Native health APIs are not accessible in a browser environment.',\n };\n }\n\n async requestAuthorization(_options: AuthorizationOptions): Promise<AuthorizationStatus> {\n throw this.unimplemented('Health permissions are only available on native platforms.');\n }\n\n async checkAuthorization(_options: AuthorizationOptions): Promise<AuthorizationStatus> {\n throw this.unimplemented('Health permissions are only available on native platforms.');\n }\n\n async readSamples(_options: QueryOptions): Promise<ReadSamplesResult> {\n throw this.unimplemented('Reading health data is only available on native platforms.');\n }\n\n async saveSample(_options: WriteSampleOptions): Promise<void> {\n throw this.unimplemented('Writing health data is only available on native platforms.');\n }\n\n async getPluginVersion(): Promise<{ version: string }> {\n return { version: 'web' };\n }\n\n async openHealthConnectSettings(): Promise<void> {\n // No-op on web - Health Connect is Android only\n }\n\n async showPrivacyPolicy(): Promise<void> {\n // No-op on web - Health Connect privacy policy is Android only\n }\n}\n"]}
1
+ {"version":3,"file":"web.js","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAc5C,MAAM,OAAO,SAAU,SAAQ,SAAS;IACtC,KAAK,CAAC,WAAW;QACf,OAAO;YACL,SAAS,EAAE,KAAK;YAChB,QAAQ,EAAE,KAAK;YACf,MAAM,EAAE,iEAAiE;SAC1E,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,oBAAoB,CAAC,QAA8B;QACvD,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC,CAAC;IACzF,CAAC;IAED,KAAK,CAAC,kBAAkB,CAAC,QAA8B;QACrD,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC,CAAC;IACzF,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,QAAsB;QACtC,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC,CAAC;IACzF,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,QAA4B;QAC3C,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC,CAAC;IACzF,CAAC;IAED,KAAK,CAAC,gBAAgB;QACpB,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC5B,CAAC;IAED,KAAK,CAAC,yBAAyB;QAC7B,gDAAgD;IAClD,CAAC;IAED,KAAK,CAAC,iBAAiB;QACrB,+DAA+D;IACjE,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,QAA8B;QAChD,MAAM,IAAI,CAAC,aAAa,CAAC,0DAA0D,CAAC,CAAC;IACvF,CAAC;CACF","sourcesContent":["import { WebPlugin } from '@capacitor/core';\n\nimport type {\n AuthorizationOptions,\n AuthorizationStatus,\n AvailabilityResult,\n HealthPlugin,\n QueryOptions,\n QueryWorkoutsOptions,\n QueryWorkoutsResult,\n ReadSamplesResult,\n WriteSampleOptions,\n} from './definitions';\n\nexport class HealthWeb extends WebPlugin implements HealthPlugin {\n async isAvailable(): Promise<AvailabilityResult> {\n return {\n available: false,\n platform: 'web',\n reason: 'Native health APIs are not accessible in a browser environment.',\n };\n }\n\n async requestAuthorization(_options: AuthorizationOptions): Promise<AuthorizationStatus> {\n throw this.unimplemented('Health permissions are only available on native platforms.');\n }\n\n async checkAuthorization(_options: AuthorizationOptions): Promise<AuthorizationStatus> {\n throw this.unimplemented('Health permissions are only available on native platforms.');\n }\n\n async readSamples(_options: QueryOptions): Promise<ReadSamplesResult> {\n throw this.unimplemented('Reading health data is only available on native platforms.');\n }\n\n async saveSample(_options: WriteSampleOptions): Promise<void> {\n throw this.unimplemented('Writing health data is only available on native platforms.');\n }\n\n async getPluginVersion(): Promise<{ version: string }> {\n return { version: 'web' };\n }\n\n async openHealthConnectSettings(): Promise<void> {\n // No-op on web - Health Connect is Android only\n }\n\n async showPrivacyPolicy(): Promise<void> {\n // No-op on web - Health Connect privacy policy is Android only\n }\n\n async queryWorkouts(_options: QueryWorkoutsOptions): Promise<QueryWorkoutsResult> {\n throw this.unimplemented('Querying workouts is only available on native platforms.');\n }\n}\n"]}
@@ -35,6 +35,9 @@ class HealthWeb extends core.WebPlugin {
35
35
  async showPrivacyPolicy() {
36
36
  // No-op on web - Health Connect privacy policy is Android only
37
37
  }
38
+ async queryWorkouts(_options) {
39
+ throw this.unimplemented('Querying workouts is only available on native platforms.');
40
+ }
38
41
  }
39
42
 
40
43
  var web = /*#__PURE__*/Object.freeze({
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.cjs.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst Health = registerPlugin('Health', {\n web: () => import('./web').then((m) => new m.HealthWeb()),\n});\nexport * from './definitions';\nexport { Health };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class HealthWeb extends WebPlugin {\n async isAvailable() {\n return {\n available: false,\n platform: 'web',\n reason: 'Native health APIs are not accessible in a browser environment.',\n };\n }\n async requestAuthorization(_options) {\n throw this.unimplemented('Health permissions are only available on native platforms.');\n }\n async checkAuthorization(_options) {\n throw this.unimplemented('Health permissions are only available on native platforms.');\n }\n async readSamples(_options) {\n throw this.unimplemented('Reading health data is only available on native platforms.');\n }\n async saveSample(_options) {\n throw this.unimplemented('Writing health data is only available on native platforms.');\n }\n async getPluginVersion() {\n return { version: 'web' };\n }\n async openHealthConnectSettings() {\n // No-op on web - Health Connect is Android only\n }\n async showPrivacyPolicy() {\n // No-op on web - Health Connect privacy policy is Android only\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;;AACK,MAAC,MAAM,GAAGA,mBAAc,CAAC,QAAQ,EAAE;AACxC,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,SAAS,EAAE,CAAC;AAC7D,CAAC;;ACFM,MAAM,SAAS,SAASC,cAAS,CAAC;AACzC,IAAI,MAAM,WAAW,GAAG;AACxB,QAAQ,OAAO;AACf,YAAY,SAAS,EAAE,KAAK;AAC5B,YAAY,QAAQ,EAAE,KAAK;AAC3B,YAAY,MAAM,EAAE,iEAAiE;AACrF,SAAS;AACT,IAAI;AACJ,IAAI,MAAM,oBAAoB,CAAC,QAAQ,EAAE;AACzC,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC;AAC9F,IAAI;AACJ,IAAI,MAAM,kBAAkB,CAAC,QAAQ,EAAE;AACvC,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC;AAC9F,IAAI;AACJ,IAAI,MAAM,WAAW,CAAC,QAAQ,EAAE;AAChC,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC;AAC9F,IAAI;AACJ,IAAI,MAAM,UAAU,CAAC,QAAQ,EAAE;AAC/B,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC;AAC9F,IAAI;AACJ,IAAI,MAAM,gBAAgB,GAAG;AAC7B,QAAQ,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE;AACjC,IAAI;AACJ,IAAI,MAAM,yBAAyB,GAAG;AACtC;AACA,IAAI;AACJ,IAAI,MAAM,iBAAiB,GAAG;AAC9B;AACA,IAAI;AACJ;;;;;;;;;"}
1
+ {"version":3,"file":"plugin.cjs.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst Health = registerPlugin('Health', {\n web: () => import('./web').then((m) => new m.HealthWeb()),\n});\nexport * from './definitions';\nexport { Health };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class HealthWeb extends WebPlugin {\n async isAvailable() {\n return {\n available: false,\n platform: 'web',\n reason: 'Native health APIs are not accessible in a browser environment.',\n };\n }\n async requestAuthorization(_options) {\n throw this.unimplemented('Health permissions are only available on native platforms.');\n }\n async checkAuthorization(_options) {\n throw this.unimplemented('Health permissions are only available on native platforms.');\n }\n async readSamples(_options) {\n throw this.unimplemented('Reading health data is only available on native platforms.');\n }\n async saveSample(_options) {\n throw this.unimplemented('Writing health data is only available on native platforms.');\n }\n async getPluginVersion() {\n return { version: 'web' };\n }\n async openHealthConnectSettings() {\n // No-op on web - Health Connect is Android only\n }\n async showPrivacyPolicy() {\n // No-op on web - Health Connect privacy policy is Android only\n }\n async queryWorkouts(_options) {\n throw this.unimplemented('Querying workouts is only available on native platforms.');\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;;AACK,MAAC,MAAM,GAAGA,mBAAc,CAAC,QAAQ,EAAE;AACxC,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,SAAS,EAAE,CAAC;AAC7D,CAAC;;ACFM,MAAM,SAAS,SAASC,cAAS,CAAC;AACzC,IAAI,MAAM,WAAW,GAAG;AACxB,QAAQ,OAAO;AACf,YAAY,SAAS,EAAE,KAAK;AAC5B,YAAY,QAAQ,EAAE,KAAK;AAC3B,YAAY,MAAM,EAAE,iEAAiE;AACrF,SAAS;AACT,IAAI;AACJ,IAAI,MAAM,oBAAoB,CAAC,QAAQ,EAAE;AACzC,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC;AAC9F,IAAI;AACJ,IAAI,MAAM,kBAAkB,CAAC,QAAQ,EAAE;AACvC,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC;AAC9F,IAAI;AACJ,IAAI,MAAM,WAAW,CAAC,QAAQ,EAAE;AAChC,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC;AAC9F,IAAI;AACJ,IAAI,MAAM,UAAU,CAAC,QAAQ,EAAE;AAC/B,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC;AAC9F,IAAI;AACJ,IAAI,MAAM,gBAAgB,GAAG;AAC7B,QAAQ,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE;AACjC,IAAI;AACJ,IAAI,MAAM,yBAAyB,GAAG;AACtC;AACA,IAAI;AACJ,IAAI,MAAM,iBAAiB,GAAG;AAC9B;AACA,IAAI;AACJ,IAAI,MAAM,aAAa,CAAC,QAAQ,EAAE;AAClC,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,0DAA0D,CAAC;AAC5F,IAAI;AACJ;;;;;;;;;"}
package/dist/plugin.js CHANGED
@@ -34,6 +34,9 @@ var capacitorHealth = (function (exports, core) {
34
34
  async showPrivacyPolicy() {
35
35
  // No-op on web - Health Connect privacy policy is Android only
36
36
  }
37
+ async queryWorkouts(_options) {
38
+ throw this.unimplemented('Querying workouts is only available on native platforms.');
39
+ }
37
40
  }
38
41
 
39
42
  var web = /*#__PURE__*/Object.freeze({
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst Health = registerPlugin('Health', {\n web: () => import('./web').then((m) => new m.HealthWeb()),\n});\nexport * from './definitions';\nexport { Health };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class HealthWeb extends WebPlugin {\n async isAvailable() {\n return {\n available: false,\n platform: 'web',\n reason: 'Native health APIs are not accessible in a browser environment.',\n };\n }\n async requestAuthorization(_options) {\n throw this.unimplemented('Health permissions are only available on native platforms.');\n }\n async checkAuthorization(_options) {\n throw this.unimplemented('Health permissions are only available on native platforms.');\n }\n async readSamples(_options) {\n throw this.unimplemented('Reading health data is only available on native platforms.');\n }\n async saveSample(_options) {\n throw this.unimplemented('Writing health data is only available on native platforms.');\n }\n async getPluginVersion() {\n return { version: 'web' };\n }\n async openHealthConnectSettings() {\n // No-op on web - Health Connect is Android only\n }\n async showPrivacyPolicy() {\n // No-op on web - Health Connect privacy policy is Android only\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;AACK,UAAC,MAAM,GAAGA,mBAAc,CAAC,QAAQ,EAAE;IACxC,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,SAAS,EAAE,CAAC;IAC7D,CAAC;;ICFM,MAAM,SAAS,SAASC,cAAS,CAAC;IACzC,IAAI,MAAM,WAAW,GAAG;IACxB,QAAQ,OAAO;IACf,YAAY,SAAS,EAAE,KAAK;IAC5B,YAAY,QAAQ,EAAE,KAAK;IAC3B,YAAY,MAAM,EAAE,iEAAiE;IACrF,SAAS;IACT,IAAI;IACJ,IAAI,MAAM,oBAAoB,CAAC,QAAQ,EAAE;IACzC,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC;IAC9F,IAAI;IACJ,IAAI,MAAM,kBAAkB,CAAC,QAAQ,EAAE;IACvC,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC;IAC9F,IAAI;IACJ,IAAI,MAAM,WAAW,CAAC,QAAQ,EAAE;IAChC,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC;IAC9F,IAAI;IACJ,IAAI,MAAM,UAAU,CAAC,QAAQ,EAAE;IAC/B,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC;IAC9F,IAAI;IACJ,IAAI,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE;IACjC,IAAI;IACJ,IAAI,MAAM,yBAAyB,GAAG;IACtC;IACA,IAAI;IACJ,IAAI,MAAM,iBAAiB,GAAG;IAC9B;IACA,IAAI;IACJ;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"plugin.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst Health = registerPlugin('Health', {\n web: () => import('./web').then((m) => new m.HealthWeb()),\n});\nexport * from './definitions';\nexport { Health };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class HealthWeb extends WebPlugin {\n async isAvailable() {\n return {\n available: false,\n platform: 'web',\n reason: 'Native health APIs are not accessible in a browser environment.',\n };\n }\n async requestAuthorization(_options) {\n throw this.unimplemented('Health permissions are only available on native platforms.');\n }\n async checkAuthorization(_options) {\n throw this.unimplemented('Health permissions are only available on native platforms.');\n }\n async readSamples(_options) {\n throw this.unimplemented('Reading health data is only available on native platforms.');\n }\n async saveSample(_options) {\n throw this.unimplemented('Writing health data is only available on native platforms.');\n }\n async getPluginVersion() {\n return { version: 'web' };\n }\n async openHealthConnectSettings() {\n // No-op on web - Health Connect is Android only\n }\n async showPrivacyPolicy() {\n // No-op on web - Health Connect privacy policy is Android only\n }\n async queryWorkouts(_options) {\n throw this.unimplemented('Querying workouts is only available on native platforms.');\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;AACK,UAAC,MAAM,GAAGA,mBAAc,CAAC,QAAQ,EAAE;IACxC,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,SAAS,EAAE,CAAC;IAC7D,CAAC;;ICFM,MAAM,SAAS,SAASC,cAAS,CAAC;IACzC,IAAI,MAAM,WAAW,GAAG;IACxB,QAAQ,OAAO;IACf,YAAY,SAAS,EAAE,KAAK;IAC5B,YAAY,QAAQ,EAAE,KAAK;IAC3B,YAAY,MAAM,EAAE,iEAAiE;IACrF,SAAS;IACT,IAAI;IACJ,IAAI,MAAM,oBAAoB,CAAC,QAAQ,EAAE;IACzC,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC;IAC9F,IAAI;IACJ,IAAI,MAAM,kBAAkB,CAAC,QAAQ,EAAE;IACvC,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC;IAC9F,IAAI;IACJ,IAAI,MAAM,WAAW,CAAC,QAAQ,EAAE;IAChC,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC;IAC9F,IAAI;IACJ,IAAI,MAAM,UAAU,CAAC,QAAQ,EAAE;IAC/B,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC;IAC9F,IAAI;IACJ,IAAI,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE;IACjC,IAAI;IACJ,IAAI,MAAM,yBAAyB,GAAG;IACtC;IACA,IAAI;IACJ,IAAI,MAAM,iBAAiB,GAAG;IAC9B;IACA,IAAI;IACJ,IAAI,MAAM,aAAa,CAAC,QAAQ,EAAE;IAClC,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,0DAA0D,CAAC;IAC5F,IAAI;IACJ;;;;;;;;;;;;;;;"}
@@ -27,6 +27,128 @@ enum HealthManagerError: LocalizedError {
27
27
  }
28
28
  }
29
29
 
30
+ enum WorkoutType: String, CaseIterable {
31
+ case running
32
+ case cycling
33
+ case walking
34
+ case swimming
35
+ case yoga
36
+ case strengthTraining
37
+ case hiking
38
+ case tennis
39
+ case basketball
40
+ case soccer
41
+ case americanFootball
42
+ case baseball
43
+ case crossTraining
44
+ case elliptical
45
+ case rowing
46
+ case stairClimbing
47
+ case traditionalStrengthTraining
48
+ case waterFitness
49
+ case waterPolo
50
+ case waterSports
51
+ case wrestling
52
+ case other
53
+
54
+ func hkWorkoutActivityType() -> HKWorkoutActivityType {
55
+ switch self {
56
+ case .running:
57
+ return .running
58
+ case .cycling:
59
+ return .cycling
60
+ case .walking:
61
+ return .walking
62
+ case .swimming:
63
+ return .swimming
64
+ case .yoga:
65
+ return .yoga
66
+ case .strengthTraining:
67
+ return .traditionalStrengthTraining
68
+ case .hiking:
69
+ return .hiking
70
+ case .tennis:
71
+ return .tennis
72
+ case .basketball:
73
+ return .basketball
74
+ case .soccer:
75
+ return .soccer
76
+ case .americanFootball:
77
+ return .americanFootball
78
+ case .baseball:
79
+ return .baseball
80
+ case .crossTraining:
81
+ return .crossTraining
82
+ case .elliptical:
83
+ return .elliptical
84
+ case .rowing:
85
+ return .rowing
86
+ case .stairClimbing:
87
+ return .stairClimbing
88
+ case .traditionalStrengthTraining:
89
+ return .traditionalStrengthTraining
90
+ case .waterFitness:
91
+ return .waterFitness
92
+ case .waterPolo:
93
+ return .waterPolo
94
+ case .waterSports:
95
+ return .waterSports
96
+ case .wrestling:
97
+ return .wrestling
98
+ case .other:
99
+ return .other
100
+ }
101
+ }
102
+
103
+ static func fromHKWorkoutActivityType(_ hkType: HKWorkoutActivityType) -> WorkoutType {
104
+ switch hkType {
105
+ case .running:
106
+ return .running
107
+ case .cycling:
108
+ return .cycling
109
+ case .walking:
110
+ return .walking
111
+ case .swimming:
112
+ return .swimming
113
+ case .yoga:
114
+ return .yoga
115
+ case .traditionalStrengthTraining:
116
+ // Map back to strengthTraining for consistency (both map to the same HK type)
117
+ return .strengthTraining
118
+ case .hiking:
119
+ return .hiking
120
+ case .tennis:
121
+ return .tennis
122
+ case .basketball:
123
+ return .basketball
124
+ case .soccer:
125
+ return .soccer
126
+ case .americanFootball:
127
+ return .americanFootball
128
+ case .baseball:
129
+ return .baseball
130
+ case .crossTraining:
131
+ return .crossTraining
132
+ case .elliptical:
133
+ return .elliptical
134
+ case .rowing:
135
+ return .rowing
136
+ case .stairClimbing:
137
+ return .stairClimbing
138
+ case .waterFitness:
139
+ return .waterFitness
140
+ case .waterPolo:
141
+ return .waterPolo
142
+ case .waterSports:
143
+ return .waterSports
144
+ case .wrestling:
145
+ return .wrestling
146
+ default:
147
+ return .other
148
+ }
149
+ }
150
+ }
151
+
30
152
  enum HealthDataType: String, CaseIterable {
31
153
  case steps
32
154
  case distance
@@ -402,6 +524,8 @@ final class Health {
402
524
  let type = try dataType.sampleType()
403
525
  set.insert(type)
404
526
  }
527
+ // Always include workout type for read access to enable workout queries
528
+ set.insert(HKObjectType.workoutType())
405
529
  return set
406
530
  }
407
531
 
@@ -413,4 +537,92 @@ final class Health {
413
537
  }
414
538
  return set
415
539
  }
540
+
541
+ func queryWorkouts(workoutTypeString: String?, startDateString: String?, endDateString: String?, limit: Int?, ascending: Bool, completion: @escaping (Result<[[String: Any]], Error>) -> Void) {
542
+ let startDate = (try? parseDate(startDateString, defaultValue: Date().addingTimeInterval(-86400))) ?? Date().addingTimeInterval(-86400)
543
+ let endDate = (try? parseDate(endDateString, defaultValue: Date())) ?? Date()
544
+
545
+ guard endDate >= startDate else {
546
+ completion(.failure(HealthManagerError.invalidDateRange))
547
+ return
548
+ }
549
+
550
+ var predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: [])
551
+
552
+ // Filter by workout type if specified
553
+ if let workoutTypeString = workoutTypeString, let workoutType = WorkoutType(rawValue: workoutTypeString) {
554
+ let hkWorkoutType = workoutType.hkWorkoutActivityType()
555
+ let typePredicate = HKQuery.predicateForWorkouts(with: hkWorkoutType)
556
+ predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, typePredicate])
557
+ }
558
+
559
+ let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: ascending)
560
+ let queryLimit = limit ?? 100
561
+
562
+ guard let workoutSampleType = HKObjectType.workoutType() as? HKSampleType else {
563
+ completion(.failure(HealthManagerError.operationFailed("Workout type is not available.")))
564
+ return
565
+ }
566
+
567
+ let query = HKSampleQuery(sampleType: workoutSampleType, predicate: predicate, limit: queryLimit, sortDescriptors: [sortDescriptor]) { [weak self] _, samples, error in
568
+ guard let self = self else { return }
569
+
570
+ if let error = error {
571
+ completion(.failure(error))
572
+ return
573
+ }
574
+
575
+ guard let workouts = samples as? [HKWorkout] else {
576
+ completion(.success([]))
577
+ return
578
+ }
579
+
580
+ let results = workouts.map { workout -> [String: Any] in
581
+ var payload: [String: Any] = [
582
+ "workoutType": WorkoutType.fromHKWorkoutActivityType(workout.workoutActivityType).rawValue,
583
+ "duration": Int(workout.duration),
584
+ "startDate": self.isoFormatter.string(from: workout.startDate),
585
+ "endDate": self.isoFormatter.string(from: workout.endDate)
586
+ ]
587
+
588
+ // Add total energy burned if available
589
+ if let totalEnergyBurned = workout.totalEnergyBurned {
590
+ let energyInKilocalories = totalEnergyBurned.doubleValue(for: HKUnit.kilocalorie())
591
+ payload["totalEnergyBurned"] = energyInKilocalories
592
+ }
593
+
594
+ // Add total distance if available
595
+ if let totalDistance = workout.totalDistance {
596
+ let distanceInMeters = totalDistance.doubleValue(for: HKUnit.meter())
597
+ payload["totalDistance"] = distanceInMeters
598
+ }
599
+
600
+ // Add source information
601
+ let source = workout.sourceRevision.source
602
+ payload["sourceName"] = source.name
603
+ payload["sourceId"] = source.bundleIdentifier
604
+
605
+ // Add metadata if available
606
+ if let metadata = workout.metadata, !metadata.isEmpty {
607
+ var metadataDict: [String: String] = [:]
608
+ for (key, value) in metadata {
609
+ if let stringValue = value as? String {
610
+ metadataDict[key] = stringValue
611
+ } else if let numberValue = value as? NSNumber {
612
+ metadataDict[key] = numberValue.stringValue
613
+ }
614
+ }
615
+ if !metadataDict.isEmpty {
616
+ payload["metadata"] = metadataDict
617
+ }
618
+ }
619
+
620
+ return payload
621
+ }
622
+
623
+ completion(.success(results))
624
+ }
625
+
626
+ healthStore.execute(query)
627
+ }
416
628
  }
@@ -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.1.4"
6
+ private let pluginVersion: String = "8.2.0"
7
7
  public let identifier = "HealthPlugin"
8
8
  public let jsName = "Health"
9
9
  public let pluginMethods: [CAPPluginMethod] = [
@@ -14,7 +14,8 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
14
14
  CAPPluginMethod(name: "saveSample", returnType: CAPPluginReturnPromise),
15
15
  CAPPluginMethod(name: "getPluginVersion", returnType: CAPPluginReturnPromise),
16
16
  CAPPluginMethod(name: "openHealthConnectSettings", returnType: CAPPluginReturnPromise),
17
- CAPPluginMethod(name: "showPrivacyPolicy", returnType: CAPPluginReturnPromise)
17
+ CAPPluginMethod(name: "showPrivacyPolicy", returnType: CAPPluginReturnPromise),
18
+ CAPPluginMethod(name: "queryWorkouts", returnType: CAPPluginReturnPromise)
18
19
  ]
19
20
 
20
21
  private let implementation = Health()
@@ -146,4 +147,29 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
146
147
  call.resolve()
147
148
  }
148
149
 
150
+ @objc func queryWorkouts(_ call: CAPPluginCall) {
151
+ let workoutType = call.getString("workoutType")
152
+ let startDate = call.getString("startDate")
153
+ let endDate = call.getString("endDate")
154
+ let limit = call.getInt("limit")
155
+ let ascending = call.getBool("ascending") ?? false
156
+
157
+ implementation.queryWorkouts(
158
+ workoutTypeString: workoutType,
159
+ startDateString: startDate,
160
+ endDateString: endDate,
161
+ limit: limit,
162
+ ascending: ascending
163
+ ) { result in
164
+ DispatchQueue.main.async {
165
+ switch result {
166
+ case let .success(workouts):
167
+ call.resolve(["workouts": workouts])
168
+ case let .failure(error):
169
+ call.reject(error.localizedDescription, nil, error)
170
+ }
171
+ }
172
+ }
173
+ }
174
+
149
175
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-health",
3
- "version": "8.1.4",
3
+ "version": "8.2.0",
4
4
  "description": "Capacitor plugin to interact with data from Apple HealthKit and Health Connect",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",