@appeeky/expo-healthkit 0.1.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.
Files changed (62) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/LICENSE +92 -0
  3. package/README.md +453 -0
  4. package/android/build.gradle +22 -0
  5. package/android/src/main/AndroidManifest.xml +74 -0
  6. package/android/src/main/java/expo/modules/healthkit/ExpoHealthKitModule.kt +228 -0
  7. package/android/src/main/java/expo/modules/healthkit/HealthConnectMapping.kt +129 -0
  8. package/android/src/main/java/expo/modules/healthkit/HealthConnectNutrition.kt +152 -0
  9. package/android/src/main/java/expo/modules/healthkit/HealthConnectRationaleActivity.kt +62 -0
  10. package/android/src/main/java/expo/modules/healthkit/HealthConnectService.kt +1054 -0
  11. package/android/src/main/java/expo/modules/healthkit/HealthConnectUnits.kt +133 -0
  12. package/android/src/main/java/expo/modules/healthkit/HealthConnectWorkouts.kt +105 -0
  13. package/android/src/main/java/expo/modules/healthkit/HealthKitExceptions.kt +30 -0
  14. package/app.plugin.js +1 -0
  15. package/build/ExpoHealthKitModule.d.ts +39 -0
  16. package/build/ExpoHealthKitModule.d.ts.map +1 -0
  17. package/build/ExpoHealthKitModule.js +35 -0
  18. package/build/ExpoHealthKitModule.js.map +1 -0
  19. package/build/ExpoHealthKitModule.web.d.ts +39 -0
  20. package/build/ExpoHealthKitModule.web.d.ts.map +1 -0
  21. package/build/ExpoHealthKitModule.web.js +105 -0
  22. package/build/ExpoHealthKitModule.web.js.map +1 -0
  23. package/build/dates.d.ts +6 -0
  24. package/build/dates.d.ts.map +1 -0
  25. package/build/dates.js +13 -0
  26. package/build/dates.js.map +1 -0
  27. package/build/errors.d.ts +9 -0
  28. package/build/errors.d.ts.map +1 -0
  29. package/build/errors.js +18 -0
  30. package/build/errors.js.map +1 -0
  31. package/build/identifiers.d.ts +418 -0
  32. package/build/identifiers.d.ts.map +1 -0
  33. package/build/identifiers.js +402 -0
  34. package/build/identifiers.js.map +1 -0
  35. package/build/index.d.ts +454 -0
  36. package/build/index.d.ts.map +1 -0
  37. package/build/index.js +418 -0
  38. package/build/index.js.map +1 -0
  39. package/build/types.d.ts +542 -0
  40. package/build/types.d.ts.map +1 -0
  41. package/build/types.js +2 -0
  42. package/build/types.js.map +1 -0
  43. package/docs/banner.jpg +0 -0
  44. package/expo-module.config.json +9 -0
  45. package/ios/ExpoHealthKit.podspec +30 -0
  46. package/ios/ExpoHealthKitModule.swift +155 -0
  47. package/ios/HealthKitAdvancedQueries.swift +458 -0
  48. package/ios/HealthKitExceptions.swift +109 -0
  49. package/ios/HealthKitIdentifiers.swift +183 -0
  50. package/ios/HealthKitRecords.swift +214 -0
  51. package/ios/HealthKitService.swift +626 -0
  52. package/ios/PrivacyInfo.xcprivacy +14 -0
  53. package/package.json +105 -0
  54. package/plugin/build/index.d.ts +30 -0
  55. package/plugin/build/index.js +138 -0
  56. package/src/ExpoHealthKitModule.ts +116 -0
  57. package/src/ExpoHealthKitModule.web.ts +140 -0
  58. package/src/dates.ts +17 -0
  59. package/src/errors.ts +22 -0
  60. package/src/identifiers.ts +494 -0
  61. package/src/index.ts +563 -0
  62. package/src/types.ts +625 -0
@@ -0,0 +1,109 @@
1
+ import ExpoModulesCore
2
+
3
+ internal final class HealthUnavailableException: Exception, @unchecked Sendable {
4
+ override var reason: String {
5
+ "HealthKit is not available on this device"
6
+ }
7
+ }
8
+
9
+ internal final class EmptyPermissionsException: Exception, @unchecked Sendable {
10
+ override var reason: String {
11
+ "Provide at least one HealthKit type in toRead or toShare"
12
+ }
13
+ }
14
+
15
+ internal final class InvalidIdentifierException: GenericException<String>, @unchecked Sendable {
16
+ override var reason: String {
17
+ "Unknown or unsupported HealthKit identifier: \(param)"
18
+ }
19
+ }
20
+
21
+ internal final class InvalidUnitException: GenericException<String>, @unchecked Sendable {
22
+ override var reason: String {
23
+ "Invalid HealthKit unit string: \(param)"
24
+ }
25
+ }
26
+
27
+ internal final class InvalidDateException: GenericException<String>, @unchecked Sendable {
28
+ override var reason: String {
29
+ "Invalid ISO-8601 date: \(param)"
30
+ }
31
+ }
32
+
33
+ internal final class InvalidSampleTypeException: GenericException<String>, @unchecked Sendable {
34
+ override var reason: String {
35
+ "Identifier is not a writable sample type: \(param)"
36
+ }
37
+ }
38
+
39
+ internal final class MissingDeleteTypeException: Exception, @unchecked Sendable {
40
+ override var reason: String {
41
+ "deleteObjects requires a type, optionally with uuid or a date range"
42
+ }
43
+ }
44
+
45
+ internal final class MissingUuidException: Exception, @unchecked Sendable {
46
+ override var reason: String {
47
+ "uuid is not a valid UUID string"
48
+ }
49
+ }
50
+
51
+ internal final class SampleNotFoundException: GenericException<String>, @unchecked Sendable {
52
+ override var reason: String {
53
+ "No HealthKit sample found for uuid: \(param)"
54
+ }
55
+ }
56
+
57
+ internal final class EmptyCorrelationException: Exception, @unchecked Sendable {
58
+ override var reason: String {
59
+ "saveCorrelation requires at least one quantity object"
60
+ }
61
+ }
62
+
63
+ internal final class HealthKitNativeException: GenericException<String>, @unchecked Sendable {
64
+ override var code: String {
65
+ "ERR_HEALTHKIT_NATIVE"
66
+ }
67
+
68
+ override var reason: String {
69
+ param
70
+ }
71
+ }
72
+
73
+ func catchingHealthKit<T>(_ work: () throws -> T) throws -> T {
74
+ var result: Result<T, Error>?
75
+ do {
76
+ try withoutActuallyEscaping(work) { escapingWork in
77
+ try EXUtilities.catchException {
78
+ do {
79
+ result = .success(try escapingWork())
80
+ } catch {
81
+ result = .failure(error)
82
+ }
83
+ }
84
+ }
85
+ } catch {
86
+ throw HealthKitNativeException(healthKitNativeMessage(error))
87
+ }
88
+
89
+ switch result {
90
+ case .success(let value):
91
+ return value
92
+ case .failure(let error):
93
+ throw error
94
+ case nil:
95
+ throw HealthKitNativeException("HealthKit failed without a result")
96
+ }
97
+ }
98
+
99
+ private func healthKitNativeMessage(_ error: Error) -> String {
100
+ let nsError = error as NSError
101
+ let name = nsError.domain
102
+ if let description = nsError.userInfo[NSLocalizedDescriptionKey] as? String, !description.isEmpty {
103
+ return name.isEmpty || description.contains(name) ? description : "\(name): \(description)"
104
+ }
105
+ if name.isEmpty {
106
+ return nsError.localizedDescription
107
+ }
108
+ return "HealthKit raised \(name). The query arguments are invalid."
109
+ }
@@ -0,0 +1,183 @@
1
+ import HealthKit
2
+
3
+ internal enum HealthKitIdentifiers {
4
+ static func objectType(for identifier: String) throws -> HKObjectType {
5
+ if identifier == "HKWorkoutTypeIdentifier" {
6
+ return HKObjectType.workoutType()
7
+ }
8
+
9
+ if identifier.hasPrefix("HKQuantityTypeIdentifier"),
10
+ let type = HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier(rawValue: identifier))
11
+ {
12
+ return type
13
+ }
14
+
15
+ if identifier.hasPrefix("HKCategoryTypeIdentifier"),
16
+ let type = HKObjectType.categoryType(forIdentifier: HKCategoryTypeIdentifier(rawValue: identifier))
17
+ {
18
+ return type
19
+ }
20
+
21
+ if identifier.hasPrefix("HKCharacteristicTypeIdentifier"),
22
+ let type = HKObjectType.characteristicType(forIdentifier: HKCharacteristicTypeIdentifier(rawValue: identifier))
23
+ {
24
+ return type
25
+ }
26
+
27
+ if identifier.hasPrefix("HKCorrelationTypeIdentifier"),
28
+ let type = HKObjectType.correlationType(forIdentifier: HKCorrelationTypeIdentifier(rawValue: identifier))
29
+ {
30
+ return type
31
+ }
32
+
33
+ if identifier == "HKElectrocardiogramTypeIdentifier" {
34
+ return HKObjectType.electrocardiogramType()
35
+ }
36
+
37
+ if identifier == "HKAudiogramSampleTypeIdentifier" {
38
+ return HKObjectType.audiogramSampleType()
39
+ }
40
+
41
+ if identifier == "HKWorkoutRouteTypeIdentifier" {
42
+ return HKSeriesType.workoutRoute()
43
+ }
44
+
45
+ if identifier == "HKDataTypeIdentifierHeartbeatSeries" {
46
+ return HKSeriesType.heartbeat()
47
+ }
48
+
49
+ if identifier == "HKActivitySummaryTypeIdentifier" {
50
+ return HKObjectType.activitySummaryType()
51
+ }
52
+
53
+ if identifier.hasPrefix("HKClinicalTypeIdentifier"),
54
+ let type = HKObjectType.clinicalType(forIdentifier: HKClinicalTypeIdentifier(rawValue: identifier))
55
+ {
56
+ return type
57
+ }
58
+
59
+ throw InvalidIdentifierException(identifier)
60
+ }
61
+
62
+ static func sampleType(for identifier: String) throws -> HKSampleType {
63
+ guard let type = try objectType(for: identifier) as? HKSampleType else {
64
+ throw InvalidSampleTypeException(identifier)
65
+ }
66
+ return type
67
+ }
68
+
69
+ static func quantityType(for identifier: String) throws -> HKQuantityType {
70
+ guard let type = try objectType(for: identifier) as? HKQuantityType else {
71
+ throw InvalidIdentifierException(identifier)
72
+ }
73
+ return type
74
+ }
75
+
76
+ static func categoryType(for identifier: String) throws -> HKCategoryType {
77
+ guard let type = try objectType(for: identifier) as? HKCategoryType else {
78
+ throw InvalidIdentifierException(identifier)
79
+ }
80
+ return type
81
+ }
82
+
83
+ static func unit(from string: String) throws -> HKUnit {
84
+ let unit = HKUnit(from: string)
85
+ if unit.unitString.isEmpty {
86
+ throw InvalidUnitException(string)
87
+ }
88
+ return unit
89
+ }
90
+
91
+ static func date(from value: String) throws -> Date {
92
+ let withFractional = ISO8601DateFormatter()
93
+ withFractional.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
94
+ if let date = withFractional.date(from: value) {
95
+ return date
96
+ }
97
+
98
+ let withoutFractional = ISO8601DateFormatter()
99
+ withoutFractional.formatOptions = [.withInternetDateTime]
100
+ if let date = withoutFractional.date(from: value) {
101
+ return date
102
+ }
103
+
104
+ throw InvalidDateException(value)
105
+ }
106
+
107
+ static func optionalDate(from value: String?) throws -> Date? {
108
+ guard let value else {
109
+ return nil
110
+ }
111
+ return try date(from: value)
112
+ }
113
+
114
+ static func isoString(from date: Date) -> String {
115
+ let formatter = ISO8601DateFormatter()
116
+ formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
117
+ return formatter.string(from: date)
118
+ }
119
+
120
+ static func queryLimit(_ limit: Int) -> Int {
121
+ limit <= 0 ? HKObjectQueryNoLimit : limit
122
+ }
123
+
124
+ static func statisticsOptions(from raw: Int, quantityType: HKQuantityType) -> HKStatisticsOptions {
125
+ if raw == 0 {
126
+ return quantityType.aggregationStyle == .cumulative ? .cumulativeSum : .discreteAverage
127
+ }
128
+
129
+ var options: HKStatisticsOptions = []
130
+ if raw & 1 != 0 { options.insert(.discreteAverage) }
131
+ if raw & 2 != 0 { options.insert(.discreteMin) }
132
+ if raw & 4 != 0 { options.insert(.discreteMax) }
133
+ if raw & 8 != 0 { options.insert(.cumulativeSum) }
134
+ if raw & 32 != 0 { options.insert(.discreteMostRecent) }
135
+ return options
136
+ }
137
+
138
+ static func displayUnit(for quantityType: HKQuantityType) -> HKUnit {
139
+ let candidates: [HKUnit] = [
140
+ HKUnit.millimeterOfMercury(),
141
+ HKUnit.percent(),
142
+ HKUnit.count().unitDivided(by: .minute()),
143
+ HKUnit.count(),
144
+ HKUnit.kilocalorie(),
145
+ HKUnit.internationalUnit(),
146
+ HKUnit.gram(),
147
+ HKUnit.literUnit(with: .milli),
148
+ HKUnit.meter(),
149
+ HKUnit.degreeCelsius(),
150
+ HKUnit.siemenUnit(with: .micro),
151
+ HKUnit.decibelAWeightedSoundPressureLevel(),
152
+ HKUnit.second()
153
+ ]
154
+ return candidates.first { quantityType.is(compatibleWith: $0) } ?? HKUnit.count()
155
+ }
156
+
157
+ static func stringifyMetadata(_ metadata: [String: Any]?) -> [String: String]? {
158
+ guard let metadata, !metadata.isEmpty else {
159
+ return nil
160
+ }
161
+
162
+ var result: [String: String] = [:]
163
+ for (key, value) in metadata {
164
+ result[key] = String(describing: value)
165
+ }
166
+ return result
167
+ }
168
+
169
+ static func encodeAnchor(_ anchor: HKQueryAnchor?) throws -> String? {
170
+ guard let anchor else {
171
+ return nil
172
+ }
173
+ let data = try NSKeyedArchiver.archivedData(withRootObject: anchor, requiringSecureCoding: true)
174
+ return data.base64EncodedString()
175
+ }
176
+
177
+ static func decodeAnchor(_ value: String?) throws -> HKQueryAnchor? {
178
+ guard let value, let data = Data(base64Encoded: value) else {
179
+ return nil
180
+ }
181
+ return try NSKeyedUnarchiver.unarchivedObject(ofClass: HKQueryAnchor.self, from: data)
182
+ }
183
+ }
@@ -0,0 +1,214 @@
1
+ import ExpoModulesCore
2
+
3
+ internal struct AuthorizationOptions: Record {
4
+ @Field var toRead: [String] = []
5
+ @Field var toShare: [String] = []
6
+ }
7
+
8
+ internal struct QuantityQueryOptions: Record {
9
+ @Field var type: String = ""
10
+ @Field var unit: String = ""
11
+ @Field var from: String?
12
+ @Field var to: String?
13
+ @Field var limit: Int = 0
14
+ @Field var ascending: Bool = false
15
+ }
16
+
17
+ internal struct CategoryQueryOptions: Record {
18
+ @Field var type: String = ""
19
+ @Field var from: String?
20
+ @Field var to: String?
21
+ @Field var limit: Int = 0
22
+ @Field var ascending: Bool = false
23
+ }
24
+
25
+ internal struct WorkoutQueryOptions: Record {
26
+ @Field var from: String?
27
+ @Field var to: String?
28
+ @Field var limit: Int = 0
29
+ @Field var ascending: Bool = false
30
+ @Field var activityType: Int?
31
+ }
32
+
33
+ internal struct StatisticsQueryOptions: Record {
34
+ @Field var type: String = ""
35
+ @Field var unit: String = ""
36
+ @Field var from: String?
37
+ @Field var to: String?
38
+ @Field var options: Int = 0
39
+ }
40
+
41
+ internal struct StatisticsCollectionQueryOptions: Record {
42
+ @Field var type: String = ""
43
+ @Field var unit: String = ""
44
+ @Field var from: String?
45
+ @Field var to: String?
46
+ @Field var options: Int = 0
47
+ @Field var year: Int = 0
48
+ @Field var month: Int = 0
49
+ @Field var day: Int = 0
50
+ @Field var hour: Int = 0
51
+ @Field var minute: Int = 0
52
+ @Field var second: Int = 0
53
+ }
54
+
55
+ internal struct AnchoredQueryOptions: Record {
56
+ @Field var type: String = ""
57
+ @Field var unit: String?
58
+ @Field var from: String?
59
+ @Field var to: String?
60
+ @Field var limit: Int = 0
61
+ @Field var anchor: String?
62
+ }
63
+
64
+ internal struct QuantitySampleInput: Record {
65
+ @Field var type: String = ""
66
+ @Field var unit: String = ""
67
+ @Field var value: Double = 0
68
+ @Field var startDate: String = ""
69
+ @Field var endDate: String = ""
70
+ @Field var metadata: [String: String]?
71
+ }
72
+
73
+ internal struct CategorySampleInput: Record {
74
+ @Field var type: String = ""
75
+ @Field var value: Int = 0
76
+ @Field var startDate: String = ""
77
+ @Field var endDate: String = ""
78
+ @Field var metadata: [String: String]?
79
+ }
80
+
81
+ internal struct WorkoutInput: Record {
82
+ @Field var activityType: Int = 0
83
+ @Field var startDate: String = ""
84
+ @Field var endDate: String = ""
85
+ @Field var energyBurned: Double?
86
+ @Field var energyBurnedUnit: String?
87
+ @Field var distance: Double?
88
+ @Field var distanceUnit: String?
89
+ @Field var metadata: [String: String]?
90
+ }
91
+
92
+ internal struct DeleteObjectsOptions: Record {
93
+ @Field var uuid: String?
94
+ @Field var type: String?
95
+ @Field var from: String?
96
+ @Field var to: String?
97
+ }
98
+
99
+ internal struct DateRangeQueryOptions: Record {
100
+ @Field var from: String?
101
+ @Field var to: String?
102
+ @Field var limit: Int = 0
103
+ @Field var ascending: Bool = false
104
+ }
105
+
106
+ internal struct ElectrocardiogramQueryOptions: Record {
107
+ @Field var from: String?
108
+ @Field var to: String?
109
+ @Field var limit: Int = 0
110
+ @Field var ascending: Bool = false
111
+ @Field var includeVoltage: Bool = false
112
+ }
113
+
114
+ internal struct ActivitySummaryQueryOptions: Record {
115
+ @Field var from: String?
116
+ @Field var to: String?
117
+ }
118
+
119
+ internal struct ClinicalRecordQueryOptions: Record {
120
+ @Field var type: String = ""
121
+ @Field var from: String?
122
+ @Field var to: String?
123
+ @Field var limit: Int = 0
124
+ @Field var ascending: Bool = false
125
+ }
126
+
127
+ internal struct WorkoutRouteQueryOptions: Record {
128
+ @Field var workoutUUID: String = ""
129
+ }
130
+
131
+ internal struct CorrelationQueryOptions: Record {
132
+ @Field var type: String = ""
133
+ @Field var from: String?
134
+ @Field var to: String?
135
+ @Field var limit: Int = 0
136
+ @Field var ascending: Bool = false
137
+ }
138
+
139
+ internal struct CorrelationInput: Record {
140
+ @Field var type: String = ""
141
+ @Field var startDate: String = ""
142
+ @Field var endDate: String = ""
143
+ @Field var objects: [QuantitySampleInput] = []
144
+ @Field var metadata: [String: String]?
145
+ }
146
+
147
+ internal struct HeartbeatSeriesQueryOptions: Record {
148
+ @Field var from: String?
149
+ @Field var to: String?
150
+ @Field var limit: Int = 0
151
+ @Field var ascending: Bool = false
152
+ @Field var includeBeats: Bool = true
153
+ }
154
+
155
+ internal struct QuantitySampleRecord: Record {
156
+ @Field var uuid: String = ""
157
+ @Field var type: String = ""
158
+ @Field var startDate: String = ""
159
+ @Field var endDate: String = ""
160
+ @Field var value: Double = 0
161
+ @Field var unit: String = ""
162
+ @Field var sourceName: String?
163
+ @Field var sourceId: String?
164
+ @Field var metadata: [String: String]?
165
+ }
166
+
167
+ internal struct CategorySampleRecord: Record {
168
+ @Field var uuid: String = ""
169
+ @Field var type: String = ""
170
+ @Field var startDate: String = ""
171
+ @Field var endDate: String = ""
172
+ @Field var value: Int = 0
173
+ @Field var sourceName: String?
174
+ @Field var sourceId: String?
175
+ @Field var metadata: [String: String]?
176
+ }
177
+
178
+ internal struct WorkoutSampleRecord: Record {
179
+ @Field var uuid: String = ""
180
+ @Field var type: String = "HKWorkoutTypeIdentifier"
181
+ @Field var startDate: String = ""
182
+ @Field var endDate: String = ""
183
+ @Field var duration: Double = 0
184
+ @Field var workoutActivityType: Int = 0
185
+ @Field var totalEnergyBurned: Double?
186
+ @Field var totalEnergyBurnedUnit: String?
187
+ @Field var totalDistance: Double?
188
+ @Field var totalDistanceUnit: String?
189
+ @Field var sourceName: String?
190
+ @Field var sourceId: String?
191
+ @Field var metadata: [String: String]?
192
+ }
193
+
194
+ internal struct DeletedSampleRecord: Record {
195
+ @Field var uuid: String = ""
196
+ @Field var type: String?
197
+ }
198
+
199
+ internal struct StatisticsRecord: Record {
200
+ @Field var startDate: String = ""
201
+ @Field var endDate: String = ""
202
+ @Field var unit: String = ""
203
+ @Field var sum: Double?
204
+ @Field var min: Double?
205
+ @Field var max: Double?
206
+ @Field var average: Double?
207
+ @Field var mostRecent: Double?
208
+ }
209
+
210
+ internal struct AnchoredQueryResultRecord: Record {
211
+ @Field var added: [QuantitySampleRecord] = []
212
+ @Field var deleted: [DeletedSampleRecord] = []
213
+ @Field var anchor: String?
214
+ }