@flomentumsolutions/capacitor-health-extended 0.0.6 → 0.0.7

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.
@@ -10,8 +10,11 @@ Pod::Spec.new do |s|
10
10
  s.homepage = package['repository']['url']
11
11
  s.author = package['author']
12
12
  s.source = { :git => package['repository']['url'], :tag => s.version.to_s }
13
- s.source_files = 'ios/Sources/**/*.{swift,h,m,c,cc,mm,cpp}'
13
+ # Only include Swift/Obj-C source files that belong to the plugin
14
+ s.source_files = 'ios/Sources/HealthPluginPlugin/**/*.{swift,h,m}'
14
15
  s.ios.deployment_target = '13.0'
15
- s.dependency 'Capacitor'
16
- s.swift_version = '5.1'
17
- end
16
+ s.dependency 'Capacitor', '~> 6.2'
17
+ s.dependency 'CapacitorCordova', '~> 6.2'
18
+ # Match the Swift shipped with Xcode 16 (use 5.9 for Xcode 15.x)
19
+ s.swift_version = '6.0'
20
+ end
package/README.md CHANGED
@@ -35,14 +35,17 @@ npx cap sync
35
35
  </queries>
36
36
 
37
37
  <!-- Declare permissions you’ll request -->
38
- <uses-permission android:name="android.permission.INTERNET" />
39
- <uses-permission android:name="android.permission.health.READ_STEPS"/>
40
- <uses-permission android:name="android.permission.health.READ_EXERCISE"/>
38
+ <uses-permission android:name="android.permission.health.READ_STEPS" />
39
+ <uses-permission android:name="android.permission.health.READ_ACTIVE_CALORIES_BURNED" />
40
+ <uses-permission android:name="android.permission.health.READ_TOTAL_CALORIES_BURNED" />
41
+ <uses-permission android:name="android.permission.health.READ_DISTANCE" />
42
+ <uses-permission android:name="android.permission.health.READ_EXERCISE" />
43
+ <uses-permission android:name="android.permission.health.READ_EXERCISE_ROUTE" />
44
+ <uses-permission android:name="android.permission.health.READ_HEART_RATE" />
41
45
  <uses-permission android:name="android.permission.health.READ_WEIGHT" />
42
- <uses-permission android:name="android.permission.health.READ_DISTANCE"/>
43
- <uses-permission android:name="android.permission.health.READ_ACTIVE_CALORIES_BURNED"/>
44
- <uses-permission android:name="android.permission.health.READ_TOTAL_CALORIES_BURNED"/>
45
- <uses-permission android:name="android.permission.health.READ_HEART_RATE"/>
46
+ <uses-permission android:name="android.permission.health.READ_HEIGHT" />
47
+ <uses-permission android:name="android.permission.health.READ_HEART_RATE_VARIABILITY" />
48
+ <uses-permission android:name="android.permission.health.READ_BLOOD_PRESSURE" />
46
49
  ```
47
50
 
48
51
 
@@ -277,12 +280,12 @@ Query workouts
277
280
 
278
281
  #### QueryAggregatedRequest
279
282
 
280
- | Prop | Type |
281
- | --------------- | ---------------------------------- |
282
- | **`startDate`** | <code>string</code> |
283
- | **`endDate`** | <code>string</code> |
284
- | **`dataType`** | <code>'steps' \| 'calories'</code> |
285
- | **`bucket`** | <code>string</code> |
283
+ | Prop | Type |
284
+ | --------------- | --------------------------------------------------------------------------------------- |
285
+ | **`startDate`** | <code>string</code> |
286
+ | **`endDate`** | <code>string</code> |
287
+ | **`dataType`** | <code>'steps' \| 'active-calories' \| 'mindfulness' \| 'hrv' \| 'blood-pressure'</code> |
288
+ | **`bucket`** | <code>string</code> |
286
289
 
287
290
 
288
291
  #### QueryWorkoutResponse
@@ -303,6 +306,7 @@ Query workouts
303
306
  | **`id`** | <code>string</code> |
304
307
  | **`duration`** | <code>number</code> |
305
308
  | **`distance`** | <code>number</code> |
309
+ | **`steps`** | <code>number</code> |
306
310
  | **`calories`** | <code>number</code> |
307
311
  | **`sourceBundleId`** | <code>string</code> |
308
312
  | **`route`** | <code>RouteSample[]</code> |
@@ -335,6 +339,7 @@ Query workouts
335
339
  | **`endDate`** | <code>string</code> |
336
340
  | **`includeHeartRate`** | <code>boolean</code> |
337
341
  | **`includeRoute`** | <code>boolean</code> |
342
+ | **`includeSteps`** | <code>boolean</code> |
338
343
 
339
344
 
340
345
  ### Type Aliases
@@ -342,6 +347,6 @@ Query workouts
342
347
 
343
348
  #### HealthPermission
344
349
 
345
- <code>'READ_STEPS' | 'READ_WORKOUTS' | 'READ_CALORIES' | 'READ_DISTANCE' | 'READ_HEART_RATE' | 'READ_ROUTE'</code>
350
+ <code>'READ_STEPS' | 'READ_WORKOUTS' | 'READ_ACTIVE_CALORIES' | 'READ_TOTAL_CALORIES' | 'READ_DISTANCE' | 'READ_WEIGHT' | 'READ_HEIGHT' | 'READ_HEART_RATE' | 'READ_ROUTE' | 'READ_MINDFULNESS' | 'READ_HRV' | 'READ_BLOOD_PRESSURE'</code>
346
351
 
347
352
  </docgen-api>
@@ -51,6 +51,7 @@ enum class CapHealthPermission {
51
51
  permissions = [
52
52
  Permission(alias = "READ_STEPS", strings = ["android.permission.health.READ_STEPS"]),
53
53
  Permission(alias = "READ_WEIGHT", strings = ["android.permission.health.READ_WEIGHT"]),
54
+ Permission(alias = "READ_HEIGHT", strings = ["android.permission.health.READ_HEIGHT"]),
54
55
  Permission(alias = "READ_WORKOUTS", strings = ["android.permission.health.READ_EXERCISE"]),
55
56
  Permission(alias = "READ_DISTANCE", strings = ["android.permission.health.READ_DISTANCE"]),
56
57
  Permission(alias = "READ_ACTIVE_CALORIES", strings = ["android.permission.health.READ_ACTIVE_CALORIES_BURNED"]),
@@ -51,7 +51,7 @@ export interface HealthPlugin {
51
51
  */
52
52
  queryWorkouts(request: QueryWorkoutRequest): Promise<QueryWorkoutResponse>;
53
53
  }
54
- export declare type HealthPermission = 'READ_STEPS' | 'READ_WORKOUTS' | 'READ_ACTIVE_CALORIES' | 'READ_TOTAL_CALORIES' | 'READ_DISTANCE' | 'READ_HEART_RATE' | 'READ_ROUTE' | 'READ_MINDFULNESS' | 'READ_HRV' | 'READ_BLOOD_PRESSURE';
54
+ export declare type HealthPermission = 'READ_STEPS' | 'READ_WORKOUTS' | 'READ_ACTIVE_CALORIES' | 'READ_TOTAL_CALORIES' | 'READ_DISTANCE' | 'READ_WEIGHT' | 'READ_HEIGHT' | 'READ_HEART_RATE' | 'READ_ROUTE' | 'READ_MINDFULNESS' | 'READ_HRV' | 'READ_BLOOD_PRESSURE';
55
55
  export interface PermissionsRequest {
56
56
  permissions: HealthPermission[];
57
57
  }
@@ -1 +1 @@
1
- {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["export interface HealthPlugin {\n /**\n * Checks if health API is available.\n * Android: If false is returned, the Google Health Connect app is probably not installed.\n * See showHealthConnectInPlayStore()\n *\n */\n isHealthAvailable(): Promise<{ available: boolean }>;\n\n /**\n * Android only: Returns for each given permission, if it was granted by the underlying health API\n * @param permissions permissions to query\n */\n checkHealthPermissions(permissions: PermissionsRequest): Promise<PermissionResponse>;\n\n /**\n * Requests the permissions from the user.\n *\n * Android: Apps can ask only a few times for permissions, after that the user has to grant them manually in\n * the Health Connect app. See openHealthConnectSettings()\n *\n * iOS: If the permissions are already granted or denied, this method will just return without asking the user. In iOS\n * we can't really detect if a user granted or denied a permission. The return value reflects the assumption that all\n * permissions were granted.\n *\n * @param permissions permissions to request\n */\n requestHealthPermissions(permissions: PermissionsRequest): Promise<PermissionResponse>;\n\n /**\n * Opens the apps settings, which is kind of wrong, because health permissions are configured under:\n * Settings > Apps > (Apple) Health > Access and Devices > [app-name]\n * But we can't go there directly.\n */\n openAppleHealthSettings(): Promise<void>;\n\n /**\n * Opens the Google Health Connect app\n */\n openHealthConnectSettings(): Promise<void>;\n\n /**\n * Opens the Google Health Connect app in PlayStore\n */\n showHealthConnectInPlayStore(): Promise<void>;\n\n /**\n * Query aggregated data\n * @param request\n */\n queryAggregated(request: QueryAggregatedRequest): Promise<QueryAggregatedResponse>;\n\n /**\n * Query workouts\n * @param request\n */\n queryWorkouts(request: QueryWorkoutRequest): Promise<QueryWorkoutResponse>;\n}\n\nexport declare type HealthPermission =\n | 'READ_STEPS'\n | 'READ_WORKOUTS'\n | 'READ_ACTIVE_CALORIES'\n | 'READ_TOTAL_CALORIES'\n | 'READ_DISTANCE'\n | 'READ_HEART_RATE'\n | 'READ_ROUTE'\n | 'READ_MINDFULNESS'\n | 'READ_HRV'\n | 'READ_BLOOD_PRESSURE';\n\nexport interface PermissionsRequest {\n permissions: HealthPermission[];\n}\n\nexport interface PermissionResponse {\n permissions: { [key: string]: boolean }[];\n}\n\nexport interface QueryWorkoutRequest {\n startDate: string;\n endDate: string;\n includeHeartRate: boolean;\n includeRoute: boolean;\n includeSteps: boolean;\n}\n\nexport interface HeartRateSample {\n timestamp: string;\n bpm: number;\n}\n\nexport interface RouteSample {\n timestamp: string;\n lat: number;\n lng: number;\n alt?: number;\n}\n\nexport interface QueryWorkoutResponse {\n workouts: Workout[];\n}\n\nexport interface Workout {\n startDate: string;\n endDate: string;\n workoutType: string;\n sourceName: string;\n id?: string;\n duration: number;\n distance?: number;\n steps?: number;\n calories: number;\n sourceBundleId: string;\n route?: RouteSample[];\n heartRate?: HeartRateSample[];\n}\n\nexport interface QueryAggregatedRequest {\n startDate: string;\n endDate: string;\n dataType: 'steps' | 'active-calories' | 'mindfulness' | 'hrv' | 'blood-pressure';\n bucket: string;\n}\n\nexport interface QueryAggregatedResponse {\n aggregatedData: AggregatedSample[];\n}\n\nexport interface AggregatedSample {\n startDate: string;\n endDate: string;\n value: number;\n}\n"]}
1
+ {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["export interface HealthPlugin {\n /**\n * Checks if health API is available.\n * Android: If false is returned, the Google Health Connect app is probably not installed.\n * See showHealthConnectInPlayStore()\n *\n */\n isHealthAvailable(): Promise<{ available: boolean }>;\n\n /**\n * Android only: Returns for each given permission, if it was granted by the underlying health API\n * @param permissions permissions to query\n */\n checkHealthPermissions(permissions: PermissionsRequest): Promise<PermissionResponse>;\n\n /**\n * Requests the permissions from the user.\n *\n * Android: Apps can ask only a few times for permissions, after that the user has to grant them manually in\n * the Health Connect app. See openHealthConnectSettings()\n *\n * iOS: If the permissions are already granted or denied, this method will just return without asking the user. In iOS\n * we can't really detect if a user granted or denied a permission. The return value reflects the assumption that all\n * permissions were granted.\n *\n * @param permissions permissions to request\n */\n requestHealthPermissions(permissions: PermissionsRequest): Promise<PermissionResponse>;\n\n /**\n * Opens the apps settings, which is kind of wrong, because health permissions are configured under:\n * Settings > Apps > (Apple) Health > Access and Devices > [app-name]\n * But we can't go there directly.\n */\n openAppleHealthSettings(): Promise<void>;\n\n /**\n * Opens the Google Health Connect app\n */\n openHealthConnectSettings(): Promise<void>;\n\n /**\n * Opens the Google Health Connect app in PlayStore\n */\n showHealthConnectInPlayStore(): Promise<void>;\n\n /**\n * Query aggregated data\n * @param request\n */\n queryAggregated(request: QueryAggregatedRequest): Promise<QueryAggregatedResponse>;\n\n /**\n * Query workouts\n * @param request\n */\n queryWorkouts(request: QueryWorkoutRequest): Promise<QueryWorkoutResponse>;\n}\n\nexport declare type HealthPermission =\n | 'READ_STEPS'\n | 'READ_WORKOUTS'\n | 'READ_ACTIVE_CALORIES'\n | 'READ_TOTAL_CALORIES'\n | 'READ_DISTANCE'\n | 'READ_WEIGHT'\n | 'READ_HEIGHT'\n | 'READ_HEART_RATE'\n | 'READ_ROUTE'\n | 'READ_MINDFULNESS'\n | 'READ_HRV'\n | 'READ_BLOOD_PRESSURE';\n\nexport interface PermissionsRequest {\n permissions: HealthPermission[];\n}\n\nexport interface PermissionResponse {\n permissions: { [key: string]: boolean }[];\n}\n\nexport interface QueryWorkoutRequest {\n startDate: string;\n endDate: string;\n includeHeartRate: boolean;\n includeRoute: boolean;\n includeSteps: boolean;\n}\n\nexport interface HeartRateSample {\n timestamp: string;\n bpm: number;\n}\n\nexport interface RouteSample {\n timestamp: string;\n lat: number;\n lng: number;\n alt?: number;\n}\n\nexport interface QueryWorkoutResponse {\n workouts: Workout[];\n}\n\nexport interface Workout {\n startDate: string;\n endDate: string;\n workoutType: string;\n sourceName: string;\n id?: string;\n duration: number;\n distance?: number;\n steps?: number;\n calories: number;\n sourceBundleId: string;\n route?: RouteSample[];\n heartRate?: HeartRateSample[];\n}\n\nexport interface QueryAggregatedRequest {\n startDate: string;\n endDate: string;\n dataType: 'steps' | 'active-calories' | 'mindfulness' | 'hrv' | 'blood-pressure';\n bucket: string;\n}\n\nexport interface QueryAggregatedResponse {\n aggregatedData: AggregatedSample[];\n}\n\nexport interface AggregatedSample {\n startDate: string;\n endDate: string;\n value: number;\n}\n"]}
@@ -148,6 +148,14 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
148
148
  return HKObjectType.quantityType(forIdentifier: .stepCount)
149
149
  case "hrv":
150
150
  return HKObjectType.quantityType(forIdentifier: .heartRateVariabilitySDNN)
151
+ case "height":
152
+ return HKObjectType.quantityType(forIdentifier: .height)
153
+ case "distance":
154
+ return HKObjectType.quantityType(forIdentifier: .distanceWalkingRunning)
155
+ case "active-calories":
156
+ return HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)
157
+ case "total-calories":
158
+ return HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)
151
159
  case "blood-pressure":
152
160
  return nil // handled above
153
161
  default:
@@ -183,8 +191,13 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
183
191
  unit = .gramUnit(with: .kilo)
184
192
  } else if dataTypeString == "hrv" {
185
193
  unit = HKUnit.secondUnit(with: .milli)
194
+ } else if dataTypeString == "distance" {
195
+ unit = HKUnit.meter()
196
+ } else if dataTypeString == "active-calories" || dataTypeString == "total-calories" {
197
+ unit = HKUnit.kilocalorie()
198
+ } else if dataTypeString == "height" {
199
+ unit = HKUnit.meter()
186
200
  }
187
-
188
201
  let value = quantitySample.quantity.doubleValue(for: unit)
189
202
  let timestamp = quantitySample.startDate.timeIntervalSince1970 * 1000
190
203
 
@@ -216,6 +229,13 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
216
229
  return [HKObjectType.quantityType(forIdentifier: .stepCount)].compactMap{$0}
217
230
  case "READ_WEIGHT":
218
231
  return [HKObjectType.quantityType(forIdentifier: .bodyMass)].compactMap{$0}
232
+ case "READ_HEIGHT":
233
+ return [HKObjectType.quantityType(forIdentifier: .height)].compactMap { $0 }
234
+ case "READ_TOTAL_CALORIES":
235
+ return [
236
+ HKObjectType.quantityType(forIdentifier: .activeEnergyBurned),
237
+ HKObjectType.quantityType(forIdentifier: .basalEnergyBurned) // iOS 16+
238
+ ].compactMap { $0 }
219
239
  case "READ_ACTIVE_CALORIES":
220
240
  return [HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)].compactMap{$0}
221
241
  case "READ_WORKOUTS":
@@ -257,6 +277,12 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
257
277
  return HKObjectType.quantityType(forIdentifier: .bodyMass)
258
278
  case "hrv":
259
279
  return HKObjectType.quantityType(forIdentifier: .heartRateVariabilitySDNN)
280
+ case "distance":
281
+ return HKObjectType.quantityType(forIdentifier: .distanceWalkingRunning) // pick one rep type
282
+ case "total-calories":
283
+ return HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)
284
+ case "height":
285
+ return HKObjectType.quantityType(forIdentifier: .height)
260
286
  default:
261
287
  return nil
262
288
  }
@@ -296,11 +322,12 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
296
322
  }
297
323
 
298
324
  let options: HKStatisticsOptions = {
299
- switch dataType.identifier {
300
- case HKQuantityTypeIdentifier.heartRate.rawValue,
301
- HKQuantityTypeIdentifier.bodyMass.rawValue:
302
- return .discreteAverage
303
- default:
325
+ switch dataType.aggregationStyle {
326
+ case .cumulative:
327
+ return .cumulativeSum
328
+ case .discrete:
329
+ return .discreteAverage // or .discreteMin / Max when needed
330
+ @unknown default:
304
331
  return .cumulativeSum
305
332
  }
306
333
  }()
@@ -312,41 +339,63 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
312
339
  anchorDate: startDate,
313
340
  intervalComponents: interval
314
341
  )
315
-
342
+
316
343
  query.initialResultsHandler = { query, result, error in
317
344
  if let error = error {
318
345
  call.reject("Error fetching aggregated data: \(error.localizedDescription)")
319
346
  return
320
347
  }
321
-
348
+
322
349
  var aggregatedSamples: [[String: Any]] = []
323
-
324
- result?.enumerateStatistics(from: startDate, to: endDate) { statistics, stop in
325
- if let sum = statistics.sumQuantity() {
326
- let startDate = statistics.startDate.timeIntervalSince1970 * 1000
327
- let endDate = statistics.endDate.timeIntervalSince1970 * 1000
328
-
329
- var value: Double = -1.0
330
- if(dataTypeString == "steps" && dataType.is(compatibleWith: HKUnit.count())) {
331
- value = sum.doubleValue(for: HKUnit.count())
332
- } else if(dataTypeString == "active-calories" && dataType.is(compatibleWith: HKUnit.kilocalorie())) {
333
- value = sum.doubleValue(for: HKUnit.kilocalorie())
334
- } else if(dataTypeString == "mindfulness" && dataType.is(compatibleWith: HKUnit.second())) {
335
- value = sum.doubleValue(for: HKUnit.second())
350
+
351
+ result?.enumerateStatistics(from: startDate, to: endDate) { statistics, _ in
352
+ // Choose sum or average based on the options we picked
353
+ let quantity: HKQuantity? = options.contains(.cumulativeSum)
354
+ ? statistics.sumQuantity()
355
+ : statistics.averageQuantity()
356
+
357
+ guard let quantity = quantity else { return }
358
+
359
+ // Time‑bounds of this bucket
360
+ let bucketStart = statistics.startDate.timeIntervalSince1970 * 1000
361
+ let bucketEnd = statistics.endDate.timeIntervalSince1970 * 1000
362
+
363
+ // Map dataType → correct HKUnit
364
+ let unit: HKUnit = {
365
+ switch dataTypeString {
366
+ case "steps":
367
+ return .count()
368
+ case "active-calories", "total-calories":
369
+ return .kilocalorie()
370
+ case "distance":
371
+ return .meter()
372
+ case "weight":
373
+ return .gramUnit(with: .kilo)
374
+ case "height":
375
+ return .meter()
376
+ case "heart-rate":
377
+ return HKUnit.count().unitDivided(by: HKUnit.minute())
378
+ case "hrv":
379
+ return HKUnit.secondUnit(with: .milli)
380
+ case "mindfulness":
381
+ return HKUnit.second()
382
+ default:
383
+ return .count()
336
384
  }
337
-
338
-
339
- aggregatedSamples.append([
340
- "startDate": startDate,
341
- "endDate": endDate,
342
- "value": value
343
- ])
344
- }
385
+ }()
386
+
387
+ let value = quantity.doubleValue(for: unit)
388
+
389
+ aggregatedSamples.append([
390
+ "startDate": bucketStart,
391
+ "endDate": bucketEnd,
392
+ "value": value
393
+ ])
345
394
  }
346
-
395
+
347
396
  call.resolve(["aggregatedData": aggregatedSamples])
348
397
  }
349
-
398
+
350
399
  healthStore.execute(query)
351
400
  }
352
401
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flomentumsolutions/capacitor-health-extended",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "Capacitor plugin for Apple HealthKit and Google Health Connect Platform",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",
@@ -19,7 +19,7 @@
19
19
  "license": "MIT",
20
20
  "repository": {
21
21
  "type": "git",
22
- "url": "git+https://github.com/Flomentum-Solutions/capacitor-health-extended.git"
22
+ "url": "https://github.com/Flomentum-Solutions/capacitor-health-extended.git"
23
23
  },
24
24
  "bugs": {
25
25
  "url": "https://github.com/Flomentum-Solutions/capacitor-health-extended/issues"
package/Package.swift DELETED
@@ -1,28 +0,0 @@
1
- // swift-tools-version: 5.9
2
- import PackageDescription
3
-
4
- let package = Package(
5
- name: "CapacitorHealthExtended",
6
- platforms: [.iOS(.v13)],
7
- products: [
8
- .library(
9
- name: "CapacitorHealthExtended",
10
- targets: ["HealthPluginPlugin"])
11
- ],
12
- dependencies: [
13
- .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", branch: "main")
14
- ],
15
- targets: [
16
- .target(
17
- name: "HealthPluginPlugin",
18
- dependencies: [
19
- .product(name: "Capacitor", package: "capacitor-swift-pm"),
20
- .product(name: "Cordova", package: "capacitor-swift-pm")
21
- ],
22
- path: "ios/Sources/HealthPluginPlugin"),
23
- .testTarget(
24
- name: "HealthPluginPluginTests",
25
- dependencies: ["HealthPluginPlugin"],
26
- path: "ios/Tests/HealthPluginPluginTests")
27
- ]
28
- )