@tryvital/vital-devices-react-native 0.2.7 → 0.3.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.
@@ -1,2 +1,3 @@
1
1
  #import <React/RCTBridgeModule.h>
2
2
  #import <React/RCTViewManager.h>
3
+ #import <React/RCTEventEmitter.h>
@@ -1,10 +1,29 @@
1
1
  #import <React/RCTBridgeModule.h>
2
+ #import <React/RCTEventEmitter.h>
2
3
 
3
4
  @interface RCT_EXTERN_MODULE(VitalDevicesReactNative, NSObject)
4
5
 
5
- RCT_EXTERN_METHOD(multiply:(float)a withB:(float)b
6
- withResolver:(RCTPromiseResolveBlock)resolve
7
- withRejecter:(RCTPromiseRejectBlock)reject)
6
+ RCT_EXTERN_METHOD(startScanForDevice:(NSString *)id
7
+ name:(NSString *)name
8
+ brand:(NSString *)brand
9
+ kind:(NSString *)kind
10
+ resolver:(RCTPromiseResolveBlock)resolve
11
+ rejecter:(RCTPromiseRejectBlock)reject)
12
+
13
+ RCT_EXTERN_METHOD(stopScanForDevice:(RCTPromiseResolveBlock)resolve
14
+ rejecter:(RCTPromiseRejectBlock)reject)
15
+
16
+ RCT_EXTERN_METHOD(pair:(NSString *)scannedDeviceId
17
+ resolver:(RCTPromiseResolveBlock)resolve
18
+ rejecter:(RCTPromiseRejectBlock)reject)
19
+
20
+ RCT_EXTERN_METHOD(startReadingBloodPressure:(NSString *)scannedDeviceId
21
+ resolver:(RCTPromiseResolveBlock)resolve
22
+ rejecter:(RCTPromiseRejectBlock)reject)
23
+
24
+ RCT_EXTERN_METHOD(startReadingGlucoseMeter:(NSString *)scannedDeviceId
25
+ resolver:(RCTPromiseResolveBlock)resolve
26
+ rejecter:(RCTPromiseRejectBlock)reject)
8
27
 
9
28
  + (BOOL)requiresMainQueueSetup
10
29
  {
@@ -1,8 +1,296 @@
1
+ import UIKit
2
+ import Combine
3
+ import CoreBluetooth
4
+ import CoreLocation
5
+ import CombineCoreBluetooth
6
+ import VitalCore
7
+ import VitalDevices
8
+
1
9
  @objc(VitalDevicesReactNative)
2
- class VitalDevicesReactNative: NSObject {
10
+ class VitalDevicesReactNative: RCTEventEmitter {
11
+
12
+ private lazy var deviceManager = DevicesManager()
13
+
14
+ private var glucoseMeterCancellable: Cancellable? = nil
15
+ private var bloodPressureCancellable: Cancellable? = nil
16
+ private var pairCancellable: Cancellable? = nil
17
+
18
+ private var scannerResultCancellable: Cancellable? = nil
19
+ private var scannedDevices: [ScannedDevice] = []
20
+
21
+ deinit {
22
+ scannerResultCancellable?.cancel()
23
+ glucoseMeterCancellable?.cancel()
24
+ bloodPressureCancellable?.cancel()
25
+ pairCancellable?.cancel()
26
+ }
27
+
28
+ @objc(startScanForDevice:name:brand:kind:resolver:rejecter:)
29
+ func startScanForDevice(_ id:String,
30
+ name: String,
31
+ brand: String,
32
+ kind: String,
33
+ resolve: @escaping RCTPromiseResolveBlock,
34
+ reject:RCTPromiseRejectBlock) -> Void {
35
+ do {
36
+ let deviceModel = DeviceModel(
37
+ id: id,
38
+ name: name,
39
+ brand: try mapStringToBrand(brand),
40
+ kind: try mapStringToKind(kind)
41
+ )
42
+
43
+ scannerResultCancellable?.cancel()
44
+ scannerResultCancellable = deviceManager.search(for:deviceModel)
45
+ .sink {[weak self] value in
46
+ self?.scannedDevices.append(value)
47
+ self?.sendEvent(withName: "ScanEvent", body:
48
+ [
49
+ "id": value.id.uuidString,
50
+ "name": value.name,
51
+ "deviceModel": [
52
+ "id": value.deviceModel.id,
53
+ "name": value.deviceModel.name,
54
+ "brand": value.deviceModel.brand.rawValue,
55
+ "kind": value.deviceModel.kind.rawValue
56
+ ]
57
+ ])
58
+ }
59
+
60
+ resolve(())
61
+ } catch VitalError.UnsupportedBrand(let errorMessage) {
62
+ resolve(["error": "UnsupportedBrand", "message": errorMessage])
63
+ } catch VitalError.UnsupportedKind(let errorMessage) {
64
+ resolve(["error": "UnsupportedKind", "message": errorMessage])
65
+ } catch {
66
+ resolve(["error": "Unknown", "message": error.localizedDescription])
67
+ }
68
+ }
69
+
70
+ @objc(stopScanForDevice:rejecter:)
71
+ func stopScanForDevice(_ resolve: @escaping RCTPromiseResolveBlock,
72
+ reject:RCTPromiseRejectBlock) -> Void {
73
+ scannerResultCancellable?.cancel()
74
+ resolve(())
75
+ }
76
+
77
+ @objc(pair:resolver:rejecter:)
78
+ func pair(_ scannedDeviceId:String,
79
+ resolve: @escaping RCTPromiseResolveBlock,
80
+ reject:RCTPromiseRejectBlock) -> Void {
81
+ let scannedDevice = scannedDevices.first(where: { $0.id == UUID(uuidString:scannedDeviceId) })
82
+
83
+ guard scannedDevice != nil else {
84
+ resolve([
85
+ "error": "DeviceNotFound",
86
+ "message": "Device not found with id \(scannedDeviceId)"
87
+ ])
88
+ return
89
+ }
90
+
91
+ pairCancellable?.cancel()
92
+ switch scannedDevice!.deviceModel.kind{
93
+ case .glucoseMeter:
94
+ pairCancellable = deviceManager
95
+ .glucoseMeter(for: scannedDevice!)
96
+ .pair(device: scannedDevice!)
97
+ .sink(receiveCompletion: {[weak self] value in
98
+ self?.handlePairCompletion(value: value )
99
+ },
100
+ receiveValue:{[weak self] value in
101
+ self?.handlePairValue()
102
+ })
103
+ case .bloodPressure:
104
+ pairCancellable = deviceManager
105
+ .bloodPressureReader(for: scannedDevice!)
106
+ .pair(device: scannedDevice!)
107
+ .sink(receiveCompletion: {[weak self] value in
108
+ self?.handlePairCompletion(value: value )
109
+ },
110
+ receiveValue:{[weak self] value in
111
+ self?.handlePairValue()
112
+ })
113
+ }
114
+ resolve(())
115
+ }
116
+
117
+ private func handlePairCompletion(value: Subscribers.Completion<any Error>){
118
+ switch value {
119
+ case .failure(let error): self.sendEvent(withName: "PairEvent", body: ["error": "PairError", "message": error.localizedDescription])
120
+ case .finished: self.sendEvent(withName: "PairEvent", body: true)
121
+ }
122
+ }
123
+
124
+ private func handlePairValue() {
125
+ self.sendEvent(withName: "PairEvent", body: true)
126
+ }
127
+
128
+ @objc(startReadingGlucoseMeter:resolver:rejecter:)
129
+ func startReadingGlucoseMeter(_ scannedDeviceId:String,
130
+ resolve: @escaping RCTPromiseResolveBlock,
131
+ reject:RCTPromiseRejectBlock) -> Void {
132
+
133
+ let scannedDeviceId = UUID(uuidString: scannedDeviceId)!
134
+ let scannedDevice = scannedDevices.first(where: { $0.id == scannedDeviceId })
135
+
136
+
137
+ guard scannedDevice != nil else {
138
+ resolve([
139
+ "error": "DeviceNotFound",
140
+ "message": "Device not found with id \(scannedDeviceId)"
141
+ ])
142
+ return
143
+ }
144
+
145
+ glucoseMeterCancellable?.cancel()
146
+ glucoseMeterCancellable = deviceManager.glucoseMeter(for :scannedDevice!)
147
+ .read(device: scannedDevice!)
148
+ .sink (receiveCompletion: {[weak self] value in
149
+ self?.sendEvent(withName: "GlucoseMeterReadEvent", body: ["error": "ReadError", "message": "error reading data from device \(value)"])
150
+ }, receiveValue:{[weak self] value in
151
+ self?.sendEvent(withName: "GlucoseMeterReadEvent", body: [
152
+ "samples": value.map({[
153
+ "id": $0.id,
154
+ "value": $0.value,
155
+ "unit": $0.unit,
156
+ "startDate": $0.startDate.timeIntervalSince1970,
157
+ "endDate": $0.endDate.timeIntervalSince1970,
158
+ "type": $0.type
159
+ ]})
160
+ ] )
161
+ })
162
+
163
+ resolve(())
164
+ }
165
+
166
+ @objc(startReadingBloodPressure:resolver:rejecter:)
167
+ func startReadingBloodPressure(_ scannedDeviceId:String,
168
+ resolve: @escaping RCTPromiseResolveBlock,
169
+ reject:RCTPromiseRejectBlock) -> Void {
170
+ let scannedDeviceId = UUID(uuidString: scannedDeviceId)!
171
+ let scannedDevice = scannedDevices.first(where: { $0.id == scannedDeviceId })
172
+
173
+ guard scannedDevice != nil else {
174
+ resolve([
175
+ "error": "DeviceNotFound",
176
+ "message": "Device not found with id \(scannedDeviceId)"
177
+ ])
178
+ return
179
+ }
3
180
 
4
- @objc(multiply:withB:withResolver:withRejecter:)
5
- func multiply(a: Float, b: Float, resolve:RCTPromiseResolveBlock,reject:RCTPromiseRejectBlock) -> Void {
6
- resolve(a*b)
181
+ bloodPressureCancellable?.cancel()
182
+ bloodPressureCancellable = deviceManager.bloodPressureReader(for :scannedDevice!)
183
+ .read(device: scannedDevice!)
184
+ .sink (receiveCompletion: {[weak self] value in
185
+ self?.sendEvent(withName: "BloodPressureReadEvent", body: ["error": "ReadError", "message": "error reading data from device \(value)"])
186
+ }, receiveValue:{[weak self] value in
187
+ self?.sendEvent(withName: "BloodPressureReadEvent", body:[
188
+ "samples": value.map({[
189
+ "systolic": [
190
+ "id": $0.systolic.id,
191
+ "value": $0.systolic.value,
192
+ "unit": $0.systolic.unit,
193
+ "startDate": $0.systolic.startDate.timeIntervalSince1970,
194
+ "endDate": $0.systolic.endDate.timeIntervalSince1970,
195
+ "type": $0.systolic.type
196
+ ],
197
+ "diastolic": [
198
+ "id": $0.diastolic.id,
199
+ "value": $0.diastolic.value,
200
+ "unit": $0.diastolic.unit,
201
+ "startDate": $0.diastolic.startDate.timeIntervalSince1970,
202
+ "endDate": $0.diastolic.endDate.timeIntervalSince1970,
203
+ "type": $0.diastolic.type
204
+ ],
205
+ "pulse": [
206
+ "id": $0.pulse?.id,
207
+ "value": $0.pulse?.value,
208
+ "unit": $0.pulse?.unit,
209
+ "startDate": $0.pulse?.startDate.timeIntervalSince1970,
210
+ "endDate": $0.pulse?.endDate.timeIntervalSince1970,
211
+ "type": $0.pulse?.type
212
+ ]
213
+ ]})
214
+ ] )
215
+ })
216
+
217
+ resolve(())
218
+ }
219
+
220
+ override func supportedEvents() -> [String]! {
221
+ return ["ScanEvent","PairEvent","GlucoseMeterReadEvent","BloodPressureReadEvent"]
222
+ }
223
+ }
224
+
225
+ private func mapStringToBrand(_ brandId: String) throws -> Brand {
226
+ switch brandId {
227
+ case "omron": return Brand.omron
228
+ case "accuChek": return Brand.accuChek
229
+ case "contour": return Brand.contour
230
+ case "beurer": return Brand.beurer
231
+ case "libre": return Brand.libre
232
+ default: throw VitalError.UnsupportedBrand(brandId)
233
+ }
234
+ }
235
+
236
+ private func mapStringToKind(_ kindId: String) throws -> DeviceModel.Kind {
237
+ switch kindId {
238
+ case "bloodPressure": return DeviceModel.Kind.bloodPressure
239
+ case "glucoseMeter": return DeviceModel.Kind.glucoseMeter
240
+ default: throw VitalError.UnsupportedKind(kindId)
241
+ }
242
+ }
243
+
244
+ private func mapBrandToString(_ brand: Brand) -> String {
245
+ switch brand {
246
+ case .omron: return "omron"
247
+ case .accuChek: return "accuChek"
248
+ case .contour: return "contour"
249
+ case .beurer: return "beurer"
250
+ case .libre: return "libre"
251
+ }
252
+ }
253
+
254
+ public struct InternalScannedDevice: Equatable, Encodable {
255
+ public let id: String
256
+ public let name: String
257
+ public let deviceModel: DeviceModel
258
+
259
+ init(
260
+ id: String,
261
+ name: String,
262
+ deviceModel: DeviceModel
263
+ ) {
264
+ self.id = id
265
+ self.name = name
266
+ self.deviceModel = deviceModel
7
267
  }
8
268
  }
269
+
270
+ struct AnyEncodable: Encodable {
271
+ let value: Encodable
272
+
273
+ func encode(to encoder: Encoder) throws {
274
+ try value.encode(to: encoder)
275
+ }
276
+ }
277
+
278
+ struct ErrorResult: Encodable {
279
+ let code: String
280
+ let message: String?
281
+
282
+ init(code: String, message: String? = nil){
283
+ self.code = code
284
+ self.message = message
285
+ }
286
+ }
287
+
288
+ enum VitalError: Error {
289
+ case UnsupportedRegion(String)
290
+ case UnsupportedEnvironment(String)
291
+ case UnsupportedResource(String)
292
+ case UnsupportedDataPushMode(String)
293
+ case UnsupportedProvider(String)
294
+ case UnsupportedBrand(String)
295
+ case UnsupportedKind(String)
296
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tryvital/vital-devices-react-native",
3
- "version": "0.2.7",
3
+ "version": "0.3.0",
4
4
  "description": "Client to access Vital's Endpoints",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -51,6 +51,10 @@
51
51
  "dependencies": {
52
52
  "react-native-permissions": "^3.6.1"
53
53
  },
54
+ "peerDependencies": {
55
+ "react": "*",
56
+ "react-native": "*"
57
+ },
54
58
  "devDependencies": {
55
59
  "@arkweid/lefthook": "^0.7.7",
56
60
  "@commitlint/config-conventional": "^17.0.2",
package/src/index.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import type { DeviceModel } from './model/device_model';
2
+ import { DeviceKind } from './model/device_model';
2
3
  import { NativeModules, Platform } from 'react-native';
3
4
  import { checkMultiple, PERMISSIONS } from 'react-native-permissions';
4
5
  import { Brand } from './model/brand';
5
- import { DeviceKind } from './model/device_model';
6
6
 
7
7
  const LINKING_ERROR =
8
8
  `The package 'vital-core-react-native' doesn't seem to be linked. Make sure: \n\n` +
@@ -24,10 +24,10 @@ const VitalDevicesReactNative = NativeModules.VitalDevicesReactNative
24
24
  export const VitalDevicesNativeModule = VitalDevicesReactNative;
25
25
 
26
26
  export const VitalDevicesEvents = {
27
- scanEvent: VitalDevicesNativeModule.ScanEvent,
28
- pairEvent: VitalDevicesNativeModule.PairEvent,
29
- glucoseMeterReadEvent: VitalDevicesNativeModule.GlucoseMeterReadEvent,
30
- bloodPressureReadEvent: VitalDevicesNativeModule.BloodPressureReadEvent,
27
+ scanEvent: 'ScanEvent',
28
+ pairEvent: 'PairEvent',
29
+ glucoseMeterReadEvent: 'GlucoseMeterReadEvent',
30
+ bloodPressureReadEvent: 'BloodPressureReadEvent',
31
31
  };
32
32
 
33
33
  export class VitalDevicesManager {
@@ -69,17 +69,29 @@ export class VitalDevicesManager {
69
69
  }
70
70
 
71
71
  private async checkPermission() {
72
- const result = await checkMultiple([
73
- PERMISSIONS.ANDROID.BLUETOOTH_SCAN,
74
- PERMISSIONS.ANDROID.BLUETOOTH_CONNECT,
75
- ]);
76
-
77
- if (result[PERMISSIONS.ANDROID.BLUETOOTH_SCAN] !== 'granted') {
78
- throw new Error('Bluetooth scan permission is not granted');
79
- }
72
+ if (Platform.OS === 'ios') {
73
+ const result = await checkMultiple([
74
+ PERMISSIONS.IOS.BLUETOOTH_PERIPHERAL,
75
+ ]);
76
+
77
+ if (result[PERMISSIONS.IOS.BLUETOOTH_PERIPHERAL] !== 'granted') {
78
+ throw new Error(
79
+ 'Bluetooth permission is not granted. Please check your settings.'
80
+ );
81
+ }
82
+ } else {
83
+ const result = await checkMultiple([
84
+ PERMISSIONS.ANDROID.BLUETOOTH_SCAN,
85
+ PERMISSIONS.ANDROID.BLUETOOTH_CONNECT,
86
+ ]);
87
+
88
+ if (result[PERMISSIONS.ANDROID.BLUETOOTH_SCAN] !== 'granted') {
89
+ throw new Error('Bluetooth scan permission is not granted');
90
+ }
80
91
 
81
- if (result[PERMISSIONS.ANDROID.BLUETOOTH_CONNECT] !== 'granted') {
82
- throw new Error('Bluetooth connect permission is not granted');
92
+ if (result[PERMISSIONS.ANDROID.BLUETOOTH_CONNECT] !== 'granted') {
93
+ throw new Error('Bluetooth connect permission is not granted');
94
+ }
83
95
  }
84
96
  }
85
97
 
@@ -17,6 +17,7 @@ Pod::Spec.new do |s|
17
17
  s.source_files = "ios/**/*.{h,m,mm,swift}"
18
18
 
19
19
  s.dependency "React-Core"
20
+ s.dependency "VitalDevices", "~> 0.7.11"
20
21
 
21
22
  # Don't install the dependencies when we run `pod install` in the old architecture.
22
23
  if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then