@capgo/capacitor-health 7.0.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/CapgoCapacitorHealth.podspec +18 -0
- package/Package.swift +31 -0
- package/README.md +273 -0
- package/android/build.gradle +72 -0
- package/android/src/main/AndroidManifest.xml +12 -0
- package/android/src/main/java/app/capgo/plugin/health/Health.java +11 -0
- package/android/src/main/java/app/capgo/plugin/health/HealthDataType.kt +34 -0
- package/android/src/main/java/app/capgo/plugin/health/HealthManager.kt +280 -0
- package/android/src/main/java/app/capgo/plugin/health/HealthPlugin.kt +316 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/dist/docs.json +457 -0
- package/dist/esm/definitions.d.ts +71 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +9 -0
- package/dist/esm/web.js +23 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +37 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +40 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/HealthPlugin/Health.swift +420 -0
- package/ios/Sources/HealthPlugin/HealthPlugin.swift +130 -0
- package/ios/Tests/HealthPluginTests/HealthPluginTests.swift +15 -0
- package/package.json +80 -0
|
@@ -0,0 +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}\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;;;;;;;;;;;;;;;"}
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import HealthKit
|
|
3
|
+
|
|
4
|
+
enum HealthManagerError: LocalizedError {
|
|
5
|
+
case healthDataUnavailable
|
|
6
|
+
case invalidDataType(String)
|
|
7
|
+
case invalidDate(String)
|
|
8
|
+
case dataTypeUnavailable(String)
|
|
9
|
+
case invalidDateRange
|
|
10
|
+
case operationFailed(String)
|
|
11
|
+
|
|
12
|
+
var errorDescription: String? {
|
|
13
|
+
switch self {
|
|
14
|
+
case .healthDataUnavailable:
|
|
15
|
+
return "Health data is not available on this device."
|
|
16
|
+
case let .invalidDataType(identifier):
|
|
17
|
+
return "Unsupported health data type: \(identifier)."
|
|
18
|
+
case let .invalidDate(dateString):
|
|
19
|
+
return "Invalid ISO 8601 date value: \(dateString)."
|
|
20
|
+
case let .dataTypeUnavailable(identifier):
|
|
21
|
+
return "The health data type \(identifier) is not available on this device."
|
|
22
|
+
case .invalidDateRange:
|
|
23
|
+
return "endDate must be greater than or equal to startDate."
|
|
24
|
+
case let .operationFailed(message):
|
|
25
|
+
return message
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
enum HealthDataType: String, CaseIterable {
|
|
31
|
+
case steps
|
|
32
|
+
case distance
|
|
33
|
+
case calories
|
|
34
|
+
case heartRate
|
|
35
|
+
case weight
|
|
36
|
+
|
|
37
|
+
func sampleType() throws -> HKQuantityType {
|
|
38
|
+
let identifier: HKQuantityTypeIdentifier
|
|
39
|
+
switch self {
|
|
40
|
+
case .steps:
|
|
41
|
+
identifier = .stepCount
|
|
42
|
+
case .distance:
|
|
43
|
+
identifier = .distanceWalkingRunning
|
|
44
|
+
case .calories:
|
|
45
|
+
identifier = .activeEnergyBurned
|
|
46
|
+
case .heartRate:
|
|
47
|
+
identifier = .heartRate
|
|
48
|
+
case .weight:
|
|
49
|
+
identifier = .bodyMass
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
guard let type = HKObjectType.quantityType(forIdentifier: identifier) else {
|
|
53
|
+
throw HealthManagerError.dataTypeUnavailable(rawValue)
|
|
54
|
+
}
|
|
55
|
+
return type
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
var defaultUnit: HKUnit {
|
|
59
|
+
switch self {
|
|
60
|
+
case .steps:
|
|
61
|
+
return HKUnit.count()
|
|
62
|
+
case .distance:
|
|
63
|
+
return HKUnit.meter()
|
|
64
|
+
case .calories:
|
|
65
|
+
return HKUnit.kilocalorie()
|
|
66
|
+
case .heartRate:
|
|
67
|
+
return HKUnit.count().unitDivided(by: HKUnit.minute())
|
|
68
|
+
case .weight:
|
|
69
|
+
return HKUnit.gramUnit(with: .kilo)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
var unitIdentifier: String {
|
|
74
|
+
switch self {
|
|
75
|
+
case .steps:
|
|
76
|
+
return "count"
|
|
77
|
+
case .distance:
|
|
78
|
+
return "meter"
|
|
79
|
+
case .calories:
|
|
80
|
+
return "kilocalorie"
|
|
81
|
+
case .heartRate:
|
|
82
|
+
return "bpm"
|
|
83
|
+
case .weight:
|
|
84
|
+
return "kilogram"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
static func parseMany(_ identifiers: [String]) throws -> [HealthDataType] {
|
|
89
|
+
try identifiers.map { identifier in
|
|
90
|
+
guard let type = HealthDataType(rawValue: identifier) else {
|
|
91
|
+
throw HealthManagerError.invalidDataType(identifier)
|
|
92
|
+
}
|
|
93
|
+
return type
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
struct AuthorizationStatusPayload {
|
|
99
|
+
let readAuthorized: [HealthDataType]
|
|
100
|
+
let readDenied: [HealthDataType]
|
|
101
|
+
let writeAuthorized: [HealthDataType]
|
|
102
|
+
let writeDenied: [HealthDataType]
|
|
103
|
+
|
|
104
|
+
func toDictionary() -> [String: Any] {
|
|
105
|
+
return [
|
|
106
|
+
"readAuthorized": readAuthorized.map { $0.rawValue },
|
|
107
|
+
"readDenied": readDenied.map { $0.rawValue },
|
|
108
|
+
"writeAuthorized": writeAuthorized.map { $0.rawValue },
|
|
109
|
+
"writeDenied": writeDenied.map { $0.rawValue }
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
final class Health {
|
|
115
|
+
private let healthStore = HKHealthStore()
|
|
116
|
+
private let isoFormatter: ISO8601DateFormatter
|
|
117
|
+
|
|
118
|
+
init() {
|
|
119
|
+
let formatter = ISO8601DateFormatter()
|
|
120
|
+
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
121
|
+
isoFormatter = formatter
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
func availabilityPayload() -> [String: Any] {
|
|
125
|
+
let available = HKHealthStore.isHealthDataAvailable()
|
|
126
|
+
if available {
|
|
127
|
+
return [
|
|
128
|
+
"available": true,
|
|
129
|
+
"platform": "ios"
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return [
|
|
134
|
+
"available": false,
|
|
135
|
+
"platform": "ios",
|
|
136
|
+
"reason": "Health data is not available on this device."
|
|
137
|
+
]
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
func requestAuthorization(readIdentifiers: [String], writeIdentifiers: [String], completion: @escaping (Result<AuthorizationStatusPayload, Error>) -> Void) {
|
|
141
|
+
guard HKHealthStore.isHealthDataAvailable() else {
|
|
142
|
+
completion(.failure(HealthManagerError.healthDataUnavailable))
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
do {
|
|
147
|
+
let readTypes = try HealthDataType.parseMany(readIdentifiers)
|
|
148
|
+
let writeTypes = try HealthDataType.parseMany(writeIdentifiers)
|
|
149
|
+
|
|
150
|
+
let readObjectTypes = try objectTypes(for: readTypes)
|
|
151
|
+
let writeSampleTypes = try sampleTypes(for: writeTypes)
|
|
152
|
+
|
|
153
|
+
healthStore.requestAuthorization(toShare: writeSampleTypes, read: readObjectTypes) { [weak self] success, error in
|
|
154
|
+
guard let self = self else { return }
|
|
155
|
+
|
|
156
|
+
if let error = error {
|
|
157
|
+
completion(.failure(error))
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if success {
|
|
162
|
+
self.evaluateAuthorizationStatus(readTypes: readTypes, writeTypes: writeTypes) { result in
|
|
163
|
+
completion(.success(result))
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
completion(.failure(HealthManagerError.operationFailed("Authorization request was not granted.")))
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} catch {
|
|
170
|
+
completion(.failure(error))
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
func checkAuthorization(readIdentifiers: [String], writeIdentifiers: [String], completion: @escaping (Result<AuthorizationStatusPayload, Error>) -> Void) {
|
|
175
|
+
do {
|
|
176
|
+
let readTypes = try HealthDataType.parseMany(readIdentifiers)
|
|
177
|
+
let writeTypes = try HealthDataType.parseMany(writeIdentifiers)
|
|
178
|
+
|
|
179
|
+
evaluateAuthorizationStatus(readTypes: readTypes, writeTypes: writeTypes) { payload in
|
|
180
|
+
completion(.success(payload))
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
completion(.failure(error))
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
func readSamples(dataTypeIdentifier: String, startDateString: String?, endDateString: String?, limit: Int?, ascending: Bool, completion: @escaping (Result<[[String: Any]], Error>) -> Void) throws {
|
|
188
|
+
let dataType = try parseDataType(identifier: dataTypeIdentifier)
|
|
189
|
+
let sampleType = try dataType.sampleType()
|
|
190
|
+
|
|
191
|
+
let startDate = try parseDate(startDateString, defaultValue: Date().addingTimeInterval(-86400))
|
|
192
|
+
let endDate = try parseDate(endDateString, defaultValue: Date())
|
|
193
|
+
|
|
194
|
+
guard endDate >= startDate else {
|
|
195
|
+
throw HealthManagerError.invalidDateRange
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: [])
|
|
199
|
+
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: ascending)
|
|
200
|
+
let queryLimit = limit ?? 100
|
|
201
|
+
|
|
202
|
+
let query = HKSampleQuery(sampleType: sampleType, predicate: predicate, limit: queryLimit, sortDescriptors: [sortDescriptor]) { [weak self] _, samples, error in
|
|
203
|
+
guard let self = self else { return }
|
|
204
|
+
|
|
205
|
+
if let error = error {
|
|
206
|
+
completion(.failure(error))
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
guard let quantitySamples = samples as? [HKQuantitySample] else {
|
|
211
|
+
completion(.success([]))
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let results = quantitySamples.map { sample -> [String: Any] in
|
|
216
|
+
let value = sample.quantity.doubleValue(for: dataType.defaultUnit)
|
|
217
|
+
var payload: [String: Any] = [
|
|
218
|
+
"dataType": dataType.rawValue,
|
|
219
|
+
"value": value,
|
|
220
|
+
"unit": dataType.unitIdentifier,
|
|
221
|
+
"startDate": self.isoFormatter.string(from: sample.startDate),
|
|
222
|
+
"endDate": self.isoFormatter.string(from: sample.endDate)
|
|
223
|
+
]
|
|
224
|
+
|
|
225
|
+
let source = sample.sourceRevision.source
|
|
226
|
+
payload["sourceName"] = source.name
|
|
227
|
+
payload["sourceId"] = source.bundleIdentifier
|
|
228
|
+
|
|
229
|
+
return payload
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
completion(.success(results))
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
healthStore.execute(query)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
func saveSample(dataTypeIdentifier: String, value: Double, unitIdentifier: String?, startDateString: String?, endDateString: String?, metadata: [String: String]?, completion: @escaping (Result<Void, Error>) -> Void) throws {
|
|
239
|
+
guard HKHealthStore.isHealthDataAvailable() else {
|
|
240
|
+
throw HealthManagerError.healthDataUnavailable
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let dataType = try parseDataType(identifier: dataTypeIdentifier)
|
|
244
|
+
let sampleType = try dataType.sampleType()
|
|
245
|
+
|
|
246
|
+
let startDate = try parseDate(startDateString, defaultValue: Date())
|
|
247
|
+
let endDate = try parseDate(endDateString, defaultValue: startDate)
|
|
248
|
+
|
|
249
|
+
guard endDate >= startDate else {
|
|
250
|
+
throw HealthManagerError.invalidDateRange
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
let unit = unit(for: unitIdentifier, dataType: dataType)
|
|
254
|
+
let quantity = HKQuantity(unit: unit, doubleValue: value)
|
|
255
|
+
|
|
256
|
+
var metadataDictionary: [String: Any]?
|
|
257
|
+
if let metadata = metadata, !metadata.isEmpty {
|
|
258
|
+
metadataDictionary = metadata.reduce(into: [String: Any]()) { result, entry in
|
|
259
|
+
result[entry.key] = entry.value
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
let sample = HKQuantitySample(type: sampleType, quantity: quantity, start: startDate, end: endDate, metadata: metadataDictionary)
|
|
264
|
+
|
|
265
|
+
healthStore.save(sample) { success, error in
|
|
266
|
+
if let error = error {
|
|
267
|
+
completion(.failure(error))
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if success {
|
|
272
|
+
completion(.success(()))
|
|
273
|
+
} else {
|
|
274
|
+
completion(.failure(HealthManagerError.operationFailed("Failed to save the sample.")))
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private func evaluateAuthorizationStatus(readTypes: [HealthDataType], writeTypes: [HealthDataType], completion: @escaping (AuthorizationStatusPayload) -> Void) {
|
|
280
|
+
let writeStatus = writeAuthorizationStatus(for: writeTypes)
|
|
281
|
+
|
|
282
|
+
readAuthorizationStatus(for: readTypes) { readAuthorized, readDenied in
|
|
283
|
+
let payload = AuthorizationStatusPayload(
|
|
284
|
+
readAuthorized: readAuthorized,
|
|
285
|
+
readDenied: readDenied,
|
|
286
|
+
writeAuthorized: writeStatus.authorized,
|
|
287
|
+
writeDenied: writeStatus.denied
|
|
288
|
+
)
|
|
289
|
+
completion(payload)
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private func writeAuthorizationStatus(for types: [HealthDataType]) -> (authorized: [HealthDataType], denied: [HealthDataType]) {
|
|
294
|
+
var authorized: [HealthDataType] = []
|
|
295
|
+
var denied: [HealthDataType] = []
|
|
296
|
+
|
|
297
|
+
for type in types {
|
|
298
|
+
guard let sampleType = try? type.sampleType() else {
|
|
299
|
+
denied.append(type)
|
|
300
|
+
continue
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
switch healthStore.authorizationStatus(for: sampleType) {
|
|
304
|
+
case .sharingAuthorized:
|
|
305
|
+
authorized.append(type)
|
|
306
|
+
case .sharingDenied, .notDetermined:
|
|
307
|
+
denied.append(type)
|
|
308
|
+
@unknown default:
|
|
309
|
+
denied.append(type)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return (authorized, denied)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private func readAuthorizationStatus(for types: [HealthDataType], completion: @escaping ([HealthDataType], [HealthDataType]) -> Void) {
|
|
317
|
+
guard !types.isEmpty else {
|
|
318
|
+
completion([], [])
|
|
319
|
+
return
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if #available(iOS 12.0, *) {
|
|
323
|
+
let group = DispatchGroup()
|
|
324
|
+
let lock = NSLock()
|
|
325
|
+
var authorized: [HealthDataType] = []
|
|
326
|
+
var denied: [HealthDataType] = []
|
|
327
|
+
|
|
328
|
+
for type in types {
|
|
329
|
+
guard let objectType = try? type.sampleType() else {
|
|
330
|
+
denied.append(type)
|
|
331
|
+
continue
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
group.enter()
|
|
335
|
+
let readSet = Set<HKObjectType>([objectType])
|
|
336
|
+
healthStore.getRequestStatusForAuthorization(toShare: Set<HKSampleType>(), read: readSet) { status, error in
|
|
337
|
+
defer { group.leave() }
|
|
338
|
+
|
|
339
|
+
if error != nil {
|
|
340
|
+
lock.lock(); denied.append(type); lock.unlock()
|
|
341
|
+
return
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
switch status {
|
|
345
|
+
case .unnecessary:
|
|
346
|
+
lock.lock(); authorized.append(type); lock.unlock()
|
|
347
|
+
case .shouldRequest, .unknown:
|
|
348
|
+
lock.lock(); denied.append(type); lock.unlock()
|
|
349
|
+
@unknown default:
|
|
350
|
+
lock.lock(); denied.append(type); lock.unlock()
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
group.notify(queue: .main) {
|
|
356
|
+
completion(authorized, denied)
|
|
357
|
+
}
|
|
358
|
+
} else {
|
|
359
|
+
completion(types, [])
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
private func parseDataType(identifier: String) throws -> HealthDataType {
|
|
364
|
+
guard let type = HealthDataType(rawValue: identifier) else {
|
|
365
|
+
throw HealthManagerError.invalidDataType(identifier)
|
|
366
|
+
}
|
|
367
|
+
return type
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private func parseDate(_ string: String?, defaultValue: Date) throws -> Date {
|
|
371
|
+
guard let value = string else {
|
|
372
|
+
return defaultValue
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if let date = isoFormatter.date(from: value) {
|
|
376
|
+
return date
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
throw HealthManagerError.invalidDate(value)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private func unit(for identifier: String?, dataType: HealthDataType) -> HKUnit {
|
|
383
|
+
guard let identifier = identifier else {
|
|
384
|
+
return dataType.defaultUnit
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
switch identifier {
|
|
388
|
+
case "count":
|
|
389
|
+
return HKUnit.count()
|
|
390
|
+
case "meter":
|
|
391
|
+
return HKUnit.meter()
|
|
392
|
+
case "kilocalorie":
|
|
393
|
+
return HKUnit.kilocalorie()
|
|
394
|
+
case "bpm":
|
|
395
|
+
return HKUnit.count().unitDivided(by: HKUnit.minute())
|
|
396
|
+
case "kilogram":
|
|
397
|
+
return HKUnit.gramUnit(with: .kilo)
|
|
398
|
+
default:
|
|
399
|
+
return dataType.defaultUnit
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
private func objectTypes(for dataTypes: [HealthDataType]) throws -> Set<HKObjectType> {
|
|
404
|
+
var set = Set<HKObjectType>()
|
|
405
|
+
for dataType in dataTypes {
|
|
406
|
+
let type = try dataType.sampleType()
|
|
407
|
+
set.insert(type)
|
|
408
|
+
}
|
|
409
|
+
return set
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
private func sampleTypes(for dataTypes: [HealthDataType]) throws -> Set<HKSampleType> {
|
|
413
|
+
var set = Set<HKSampleType>()
|
|
414
|
+
for dataType in dataTypes {
|
|
415
|
+
let type = try dataType.sampleType() as HKSampleType
|
|
416
|
+
set.insert(type)
|
|
417
|
+
}
|
|
418
|
+
return set
|
|
419
|
+
}
|
|
420
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Capacitor
|
|
3
|
+
|
|
4
|
+
@objc(HealthPlugin)
|
|
5
|
+
public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
6
|
+
public let identifier = "HealthPlugin"
|
|
7
|
+
public let jsName = "Health"
|
|
8
|
+
public let pluginMethods: [CAPPluginMethod] = [
|
|
9
|
+
CAPPluginMethod(name: "isAvailable", returnType: CAPPluginReturnPromise),
|
|
10
|
+
CAPPluginMethod(name: "requestAuthorization", returnType: CAPPluginReturnPromise),
|
|
11
|
+
CAPPluginMethod(name: "checkAuthorization", returnType: CAPPluginReturnPromise),
|
|
12
|
+
CAPPluginMethod(name: "readSamples", returnType: CAPPluginReturnPromise),
|
|
13
|
+
CAPPluginMethod(name: "saveSample", returnType: CAPPluginReturnPromise)
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
private let implementation = Health()
|
|
17
|
+
|
|
18
|
+
@objc func isAvailable(_ call: CAPPluginCall) {
|
|
19
|
+
call.resolve(implementation.availabilityPayload())
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@objc func requestAuthorization(_ call: CAPPluginCall) {
|
|
23
|
+
let read = (call.getArray("read") as? [String]) ?? []
|
|
24
|
+
let write = (call.getArray("write") as? [String]) ?? []
|
|
25
|
+
|
|
26
|
+
implementation.requestAuthorization(readIdentifiers: read, writeIdentifiers: write) { result in
|
|
27
|
+
DispatchQueue.main.async {
|
|
28
|
+
switch result {
|
|
29
|
+
case let .success(payload):
|
|
30
|
+
call.resolve(payload.toDictionary())
|
|
31
|
+
case let .failure(error):
|
|
32
|
+
call.reject(error.localizedDescription, nil, error)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@objc func checkAuthorization(_ call: CAPPluginCall) {
|
|
39
|
+
let read = (call.getArray("read") as? [String]) ?? []
|
|
40
|
+
let write = (call.getArray("write") as? [String]) ?? []
|
|
41
|
+
|
|
42
|
+
implementation.checkAuthorization(readIdentifiers: read, writeIdentifiers: write) { result in
|
|
43
|
+
DispatchQueue.main.async {
|
|
44
|
+
switch result {
|
|
45
|
+
case let .success(payload):
|
|
46
|
+
call.resolve(payload.toDictionary())
|
|
47
|
+
case let .failure(error):
|
|
48
|
+
call.reject(error.localizedDescription, nil, error)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@objc func readSamples(_ call: CAPPluginCall) {
|
|
55
|
+
guard let dataType = call.getString("dataType") else {
|
|
56
|
+
call.reject("dataType is required")
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let startDate = call.getString("startDate")
|
|
61
|
+
let endDate = call.getString("endDate")
|
|
62
|
+
let limit = call.getInt("limit")
|
|
63
|
+
let ascending = call.getBool("ascending") ?? false
|
|
64
|
+
|
|
65
|
+
do {
|
|
66
|
+
try implementation.readSamples(
|
|
67
|
+
dataTypeIdentifier: dataType,
|
|
68
|
+
startDateString: startDate,
|
|
69
|
+
endDateString: endDate,
|
|
70
|
+
limit: limit,
|
|
71
|
+
ascending: ascending
|
|
72
|
+
) { result in
|
|
73
|
+
DispatchQueue.main.async {
|
|
74
|
+
switch result {
|
|
75
|
+
case let .success(samples):
|
|
76
|
+
call.resolve(["samples": samples])
|
|
77
|
+
case let .failure(error):
|
|
78
|
+
call.reject(error.localizedDescription, nil, error)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
call.reject(error.localizedDescription, nil, error)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@objc func saveSample(_ call: CAPPluginCall) {
|
|
88
|
+
guard let dataType = call.getString("dataType") else {
|
|
89
|
+
call.reject("dataType is required")
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
guard let value = call.getDouble("value") else {
|
|
94
|
+
call.reject("value is required")
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let unit = call.getString("unit")
|
|
99
|
+
let startDate = call.getString("startDate")
|
|
100
|
+
let endDate = call.getString("endDate")
|
|
101
|
+
let metadataAny = call.getObject("metadata") as? [String: Any]
|
|
102
|
+
let metadata = metadataAny?.reduce(into: [String: String]()) { result, entry in
|
|
103
|
+
if let stringValue = entry.value as? String {
|
|
104
|
+
result[entry.key] = stringValue
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
do {
|
|
109
|
+
try implementation.saveSample(
|
|
110
|
+
dataTypeIdentifier: dataType,
|
|
111
|
+
value: value,
|
|
112
|
+
unitIdentifier: unit,
|
|
113
|
+
startDateString: startDate,
|
|
114
|
+
endDateString: endDate,
|
|
115
|
+
metadata: metadata
|
|
116
|
+
) { result in
|
|
117
|
+
DispatchQueue.main.async {
|
|
118
|
+
switch result {
|
|
119
|
+
case .success:
|
|
120
|
+
call.resolve()
|
|
121
|
+
case let .failure(error):
|
|
122
|
+
call.reject(error.localizedDescription, nil, error)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
call.reject(error.localizedDescription, nil, error)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import HealthPlugin
|
|
3
|
+
|
|
4
|
+
class HealthTests: XCTestCase {
|
|
5
|
+
func testEcho() {
|
|
6
|
+
// This is an example of a functional test case for a plugin.
|
|
7
|
+
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
|
8
|
+
|
|
9
|
+
let implementation = Health()
|
|
10
|
+
let value = "Hello, World!"
|
|
11
|
+
let result = implementation.echo(value)
|
|
12
|
+
|
|
13
|
+
XCTAssertEqual(value, result)
|
|
14
|
+
}
|
|
15
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@capgo/capacitor-health",
|
|
3
|
+
"version": "7.0.0",
|
|
4
|
+
"description": "Capacitor plugin to interact with data from Apple HealthKit and Health Connect",
|
|
5
|
+
"main": "dist/plugin.cjs.js",
|
|
6
|
+
"module": "dist/esm/index.js",
|
|
7
|
+
"types": "dist/esm/index.d.ts",
|
|
8
|
+
"unpkg": "dist/plugin.js",
|
|
9
|
+
"files": [
|
|
10
|
+
"android/src/main/",
|
|
11
|
+
"android/build.gradle",
|
|
12
|
+
"dist/",
|
|
13
|
+
"ios/Sources",
|
|
14
|
+
"ios/Tests",
|
|
15
|
+
"Package.swift",
|
|
16
|
+
"CapgoCapacitorHealth.podspec"
|
|
17
|
+
],
|
|
18
|
+
"author": "Martin Donadieu <martin@capgo.app>",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/Cap-go/capacitor-health.git"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/Cap-go/capacitor-health/issues"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"capacitor",
|
|
29
|
+
"plugin",
|
|
30
|
+
"native"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"verify": "npm run verify:ios && npm run verify:android && npm run verify:web",
|
|
34
|
+
"verify:ios": "xcodebuild -scheme CapgoCapacitorHealth -destination generic/platform=iOS",
|
|
35
|
+
"verify:android": "cd android && ./gradlew clean build test && cd ..",
|
|
36
|
+
"verify:web": "npm run build",
|
|
37
|
+
"lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint",
|
|
38
|
+
"fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --fix --format",
|
|
39
|
+
"eslint": "eslint . --ext ts",
|
|
40
|
+
"prettier": "prettier \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java",
|
|
41
|
+
"swiftlint": "node-swiftlint",
|
|
42
|
+
"docgen": "docgen --api HealthPlugin --output-readme README.md --output-json dist/docs.json",
|
|
43
|
+
"build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs",
|
|
44
|
+
"clean": "rimraf ./dist",
|
|
45
|
+
"watch": "tsc --watch",
|
|
46
|
+
"prepublishOnly": "npm run build"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@capacitor/android": "^7.0.0",
|
|
50
|
+
"@capacitor/core": "^7.0.0",
|
|
51
|
+
"@capacitor/docgen": "^0.3.0",
|
|
52
|
+
"@capacitor/ios": "^7.0.0",
|
|
53
|
+
"@ionic/eslint-config": "^0.4.0",
|
|
54
|
+
"@ionic/prettier-config": "^4.0.0",
|
|
55
|
+
"@ionic/swiftlint-config": "^2.0.0",
|
|
56
|
+
"eslint": "^8.57.0",
|
|
57
|
+
"prettier": "^3.4.2",
|
|
58
|
+
"prettier-plugin-java": "^2.6.6",
|
|
59
|
+
"rimraf": "^6.0.1",
|
|
60
|
+
"rollup": "^4.30.1",
|
|
61
|
+
"swiftlint": "^2.0.0",
|
|
62
|
+
"typescript": "~4.1.5"
|
|
63
|
+
},
|
|
64
|
+
"peerDependencies": {
|
|
65
|
+
"@capacitor/core": ">=7.0.0"
|
|
66
|
+
},
|
|
67
|
+
"prettier": "@ionic/prettier-config",
|
|
68
|
+
"swiftlint": "@ionic/swiftlint-config",
|
|
69
|
+
"eslintConfig": {
|
|
70
|
+
"extends": "@ionic/eslint-config/recommended"
|
|
71
|
+
},
|
|
72
|
+
"capacitor": {
|
|
73
|
+
"ios": {
|
|
74
|
+
"src": "ios"
|
|
75
|
+
},
|
|
76
|
+
"android": {
|
|
77
|
+
"src": "android"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|