react-native-ble-nitro 1.10.3 → 1.12.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 (39) hide show
  1. package/android/src/main/cpp/cpp-adapter.cpp +4 -1
  2. package/android/src/main/java/com/margelo/nitro/co/zyke/ble/BleNitroBleManager.kt +58 -21
  3. package/ios/BleNitroBleManager.swift +60 -3
  4. package/ios/BlePeripheralDelegate.swift +68 -9
  5. package/lib/commonjs/index.d.ts +1 -1
  6. package/lib/commonjs/index.d.ts.map +1 -1
  7. package/lib/commonjs/index.js +2 -1
  8. package/lib/commonjs/index.js.map +1 -1
  9. package/lib/commonjs/manager.d.ts +19 -4
  10. package/lib/commonjs/manager.d.ts.map +1 -1
  11. package/lib/commonjs/manager.js +68 -33
  12. package/lib/commonjs/manager.js.map +1 -1
  13. package/lib/commonjs/specs/NativeBleNitro.nitro.d.ts +3 -0
  14. package/lib/commonjs/specs/NativeBleNitro.nitro.d.ts.map +1 -1
  15. package/lib/index.d.ts +1 -1
  16. package/lib/index.js +1 -1
  17. package/lib/manager.d.ts +19 -4
  18. package/lib/manager.js +66 -32
  19. package/lib/specs/NativeBleNitro.nitro.d.ts +3 -0
  20. package/nitrogen/generated/android/BleNitroOnLoad.cpp +48 -32
  21. package/nitrogen/generated/android/BleNitroOnLoad.hpp +13 -4
  22. package/nitrogen/generated/android/c++/JHybridNativeBleNitroFactorySpec.cpp +20 -26
  23. package/nitrogen/generated/android/c++/JHybridNativeBleNitroFactorySpec.hpp +19 -22
  24. package/nitrogen/generated/android/c++/JHybridNativeBleNitroSpec.cpp +52 -49
  25. package/nitrogen/generated/android/c++/JHybridNativeBleNitroSpec.hpp +21 -22
  26. package/nitrogen/generated/android/kotlin/com/margelo/nitro/co/zyke/ble/HybridNativeBleNitroFactorySpec.kt +15 -18
  27. package/nitrogen/generated/android/kotlin/com/margelo/nitro/co/zyke/ble/HybridNativeBleNitroSpec.kt +28 -18
  28. package/nitrogen/generated/android/kotlin/com/margelo/nitro/co/zyke/ble/Variant_NullType_BLEDevice.kt +0 -6
  29. package/nitrogen/generated/android/kotlin/com/margelo/nitro/co/zyke/ble/Variant_NullType_String.kt +0 -6
  30. package/nitrogen/generated/ios/c++/HybridNativeBleNitroSpecSwift.hpp +14 -0
  31. package/nitrogen/generated/ios/swift/HybridNativeBleNitroSpec.swift +2 -0
  32. package/nitrogen/generated/ios/swift/HybridNativeBleNitroSpec_cxx.swift +28 -0
  33. package/nitrogen/generated/shared/c++/HybridNativeBleNitroSpec.cpp +2 -0
  34. package/nitrogen/generated/shared/c++/HybridNativeBleNitroSpec.hpp +2 -0
  35. package/package.json +9 -9
  36. package/src/__tests__/index.test.ts +145 -1
  37. package/src/index.ts +1 -0
  38. package/src/manager.ts +96 -34
  39. package/src/specs/NativeBleNitro.nitro.ts +3 -0
@@ -24,23 +24,6 @@ import com.margelo.nitro.core.HybridObject
24
24
  "LocalVariableName", "PropertyName", "PrivatePropertyName", "FunctionName"
25
25
  )
26
26
  abstract class HybridNativeBleNitroFactorySpec: HybridObject() {
27
- @DoNotStrip
28
- private var mHybridData: HybridData = initHybrid()
29
-
30
- init {
31
- super.updateNative(mHybridData)
32
- }
33
-
34
- override fun updateNative(hybridData: HybridData) {
35
- mHybridData = hybridData
36
- super.updateNative(hybridData)
37
- }
38
-
39
- // Default implementation of `HybridObject.toString()`
40
- override fun toString(): String {
41
- return "[HybridObject NativeBleNitroFactory]"
42
- }
43
-
44
27
  // Properties
45
28
 
46
29
 
@@ -54,7 +37,21 @@ abstract class HybridNativeBleNitroFactorySpec: HybridObject() {
54
37
  return __result
55
38
  }
56
39
 
57
- private external fun initHybrid(): HybridData
40
+ // Default implementation of `HybridObject.toString()`
41
+ override fun toString(): String {
42
+ return "[HybridObject NativeBleNitroFactory]"
43
+ }
44
+
45
+ // C++ backing class
46
+ @DoNotStrip
47
+ @Keep
48
+ protected open class CxxPart(javaPart: HybridNativeBleNitroFactorySpec): HybridObject.CxxPart(javaPart) {
49
+ // C++ JHybridNativeBleNitroFactorySpec::CxxPart::initHybrid(...)
50
+ external override fun initHybrid(): HybridData
51
+ }
52
+ override fun createCxxPart(): CxxPart {
53
+ return CxxPart(this)
54
+ }
58
55
 
59
56
  companion object {
60
57
  protected const val TAG = "HybridNativeBleNitroFactorySpec"
@@ -27,23 +27,6 @@ import com.margelo.nitro.core.HybridObject
27
27
  "LocalVariableName", "PropertyName", "PrivatePropertyName", "FunctionName"
28
28
  )
29
29
  abstract class HybridNativeBleNitroSpec: HybridObject() {
30
- @DoNotStrip
31
- private var mHybridData: HybridData = initHybrid()
32
-
33
- init {
34
- super.updateNative(mHybridData)
35
- }
36
-
37
- override fun updateNative(hybridData: HybridData) {
38
- mHybridData = hybridData
39
- super.updateNative(hybridData)
40
- }
41
-
42
- // Default implementation of `HybridObject.toString()`
43
- override fun toString(): String {
44
- return "[HybridObject NativeBleNitro]"
45
- }
46
-
47
30
  // Properties
48
31
  @get:DoNotStrip
49
32
  @get:Keep
@@ -130,6 +113,15 @@ abstract class HybridNativeBleNitroSpec: HybridObject() {
130
113
  return __result
131
114
  }
132
115
 
116
+ abstract fun discoverServicesWithCharacteristics(deviceId: String, callback: (success: Boolean, error: String) -> Unit): Unit
117
+
118
+ @DoNotStrip
119
+ @Keep
120
+ private fun discoverServicesWithCharacteristics_cxx(deviceId: String, callback: Func_void_bool_std__string): Unit {
121
+ val __result = discoverServicesWithCharacteristics(deviceId, callback)
122
+ return __result
123
+ }
124
+
133
125
  @DoNotStrip
134
126
  @Keep
135
127
  abstract fun getServices(deviceId: String): Array<String>
@@ -174,6 +166,10 @@ abstract class HybridNativeBleNitroSpec: HybridObject() {
174
166
  return __result
175
167
  }
176
168
 
169
+ @DoNotStrip
170
+ @Keep
171
+ abstract fun isSubscribedToCharacteristic(deviceId: String, serviceId: String, characteristicId: String): Boolean
172
+
177
173
  abstract fun requestBluetoothEnable(callback: (success: Boolean, error: String) -> Unit): Unit
178
174
 
179
175
  @DoNotStrip
@@ -204,7 +200,21 @@ abstract class HybridNativeBleNitroSpec: HybridObject() {
204
200
  @Keep
205
201
  abstract fun openSettings(): Promise<Unit>
206
202
 
207
- private external fun initHybrid(): HybridData
203
+ // Default implementation of `HybridObject.toString()`
204
+ override fun toString(): String {
205
+ return "[HybridObject NativeBleNitro]"
206
+ }
207
+
208
+ // C++ backing class
209
+ @DoNotStrip
210
+ @Keep
211
+ protected open class CxxPart(javaPart: HybridNativeBleNitroSpec): HybridObject.CxxPart(javaPart) {
212
+ // C++ JHybridNativeBleNitroSpec::CxxPart::initHybrid(...)
213
+ external override fun initHybrid(): HybridData
214
+ }
215
+ override fun createCxxPart(): CxxPart {
216
+ return CxxPart(this)
217
+ }
208
218
 
209
219
  companion object {
210
220
  protected const val TAG = "HybridNativeBleNitroSpec"
@@ -21,12 +21,6 @@ sealed class Variant_NullType_BLEDevice {
21
21
  @DoNotStrip
22
22
  data class Second(@DoNotStrip val value: BLEDevice): Variant_NullType_BLEDevice()
23
23
 
24
- @Deprecated("getAs() is not type-safe. Use fold/asFirstOrNull/asSecondOrNull instead.", level = DeprecationLevel.ERROR)
25
- inline fun <reified T> getAs(): T? = when (this) {
26
- is First -> value as? T
27
- is Second -> value as? T
28
- }
29
-
30
24
  val isFirst: Boolean
31
25
  get() = this is First
32
26
  val isSecond: Boolean
@@ -21,12 +21,6 @@ sealed class Variant_NullType_String {
21
21
  @DoNotStrip
22
22
  data class Second(@DoNotStrip val value: String): Variant_NullType_String()
23
23
 
24
- @Deprecated("getAs() is not type-safe. Use fold/asFirstOrNull/asSecondOrNull instead.", level = DeprecationLevel.ERROR)
25
- inline fun <reified T> getAs(): T? = when (this) {
26
- is First -> value as? T
27
- is Second -> value as? T
28
- }
29
-
30
24
  val isFirst: Boolean
31
25
  get() = this is First
32
26
  val isSecond: Boolean
@@ -184,6 +184,12 @@ namespace margelo::nitro::co::zyke::ble {
184
184
  std::rethrow_exception(__result.error());
185
185
  }
186
186
  }
187
+ inline void discoverServicesWithCharacteristics(const std::string& deviceId, const std::function<void(bool /* success */, const std::string& /* error */)>& callback) override {
188
+ auto __result = _swiftPart.discoverServicesWithCharacteristics(deviceId, callback);
189
+ if (__result.hasError()) [[unlikely]] {
190
+ std::rethrow_exception(__result.error());
191
+ }
192
+ }
187
193
  inline std::vector<std::string> getServices(const std::string& deviceId) override {
188
194
  auto __result = _swiftPart.getServices(deviceId);
189
195
  if (__result.hasError()) [[unlikely]] {
@@ -224,6 +230,14 @@ namespace margelo::nitro::co::zyke::ble {
224
230
  std::rethrow_exception(__result.error());
225
231
  }
226
232
  }
233
+ inline bool isSubscribedToCharacteristic(const std::string& deviceId, const std::string& serviceId, const std::string& characteristicId) override {
234
+ auto __result = _swiftPart.isSubscribedToCharacteristic(deviceId, serviceId, characteristicId);
235
+ if (__result.hasError()) [[unlikely]] {
236
+ std::rethrow_exception(__result.error());
237
+ }
238
+ auto __value = std::move(__result.value());
239
+ return __value;
240
+ }
227
241
  inline void requestBluetoothEnable(const std::function<void(bool /* success */, const std::string& /* error */)>& callback) override {
228
242
  auto __result = _swiftPart.requestBluetoothEnable(callback);
229
243
  if (__result.hasError()) [[unlikely]] {
@@ -25,12 +25,14 @@ public protocol HybridNativeBleNitroSpec_protocol: HybridObject {
25
25
  func requestMTU(deviceId: String, mtu: Double) throws -> Double
26
26
  func readRSSI(deviceId: String, callback: @escaping (_ success: Bool, _ rssi: Double, _ error: String) -> Void) throws -> Void
27
27
  func discoverServices(deviceId: String, callback: @escaping (_ success: Bool, _ error: String) -> Void) throws -> Void
28
+ func discoverServicesWithCharacteristics(deviceId: String, callback: @escaping (_ success: Bool, _ error: String) -> Void) throws -> Void
28
29
  func getServices(deviceId: String) throws -> [String]
29
30
  func getCharacteristics(deviceId: String, serviceId: String) throws -> [String]
30
31
  func readCharacteristic(deviceId: String, serviceId: String, characteristicId: String, callback: @escaping (_ success: Bool, _ data: ArrayBuffer, _ error: String) -> Void) throws -> Void
31
32
  func writeCharacteristic(deviceId: String, serviceId: String, characteristicId: String, data: ArrayBuffer, withResponse: Bool, callback: @escaping (_ success: Bool, _ responseData: ArrayBuffer, _ error: String) -> Void) throws -> Void
32
33
  func subscribeToCharacteristic(deviceId: String, serviceId: String, characteristicId: String, updateCallback: @escaping (_ characteristicId: String, _ data: ArrayBuffer) -> Void, completionCallback: @escaping (_ success: Bool, _ error: String) -> Void) throws -> Void
33
34
  func unsubscribeFromCharacteristic(deviceId: String, serviceId: String, characteristicId: String, callback: @escaping (_ success: Bool, _ error: String) -> Void) throws -> Void
35
+ func isSubscribedToCharacteristic(deviceId: String, serviceId: String, characteristicId: String) throws -> Bool
34
36
  func requestBluetoothEnable(callback: @escaping (_ success: Bool, _ error: String) -> Void) throws -> Void
35
37
  func state() throws -> BLEState
36
38
  func subscribeToStateChange(stateCallback: @escaping (_ state: BLEState) -> Void) throws -> OperationResult
@@ -370,6 +370,22 @@ open class HybridNativeBleNitroSpec_cxx {
370
370
  }
371
371
  }
372
372
 
373
+ @inline(__always)
374
+ public final func discoverServicesWithCharacteristics(deviceId: std.string, callback: bridge.Func_void_bool_std__string) -> bridge.Result_void_ {
375
+ do {
376
+ try self.__implementation.discoverServicesWithCharacteristics(deviceId: String(deviceId), callback: { () -> (Bool, String) -> Void in
377
+ let __wrappedFunction = bridge.wrap_Func_void_bool_std__string(callback)
378
+ return { (__success: Bool, __error: String) -> Void in
379
+ __wrappedFunction.call(__success, std.string(__error))
380
+ }
381
+ }())
382
+ return bridge.create_Result_void_()
383
+ } catch (let __error) {
384
+ let __exceptionPtr = __error.toCpp()
385
+ return bridge.create_Result_void_(__exceptionPtr)
386
+ }
387
+ }
388
+
373
389
  @inline(__always)
374
390
  public final func getServices(deviceId: std.string) -> bridge.Result_std__vector_std__string__ {
375
391
  do {
@@ -475,6 +491,18 @@ open class HybridNativeBleNitroSpec_cxx {
475
491
  }
476
492
  }
477
493
 
494
+ @inline(__always)
495
+ public final func isSubscribedToCharacteristic(deviceId: std.string, serviceId: std.string, characteristicId: std.string) -> bridge.Result_bool_ {
496
+ do {
497
+ let __result = try self.__implementation.isSubscribedToCharacteristic(deviceId: String(deviceId), serviceId: String(serviceId), characteristicId: String(characteristicId))
498
+ let __resultCpp = __result
499
+ return bridge.create_Result_bool_(__resultCpp)
500
+ } catch (let __error) {
501
+ let __exceptionPtr = __error.toCpp()
502
+ return bridge.create_Result_bool_(__exceptionPtr)
503
+ }
504
+ }
505
+
478
506
  @inline(__always)
479
507
  public final func requestBluetoothEnable(callback: bridge.Func_void_bool_std__string) -> bridge.Result_void_ {
480
508
  do {
@@ -28,12 +28,14 @@ namespace margelo::nitro::co::zyke::ble {
28
28
  prototype.registerHybridMethod("requestMTU", &HybridNativeBleNitroSpec::requestMTU);
29
29
  prototype.registerHybridMethod("readRSSI", &HybridNativeBleNitroSpec::readRSSI);
30
30
  prototype.registerHybridMethod("discoverServices", &HybridNativeBleNitroSpec::discoverServices);
31
+ prototype.registerHybridMethod("discoverServicesWithCharacteristics", &HybridNativeBleNitroSpec::discoverServicesWithCharacteristics);
31
32
  prototype.registerHybridMethod("getServices", &HybridNativeBleNitroSpec::getServices);
32
33
  prototype.registerHybridMethod("getCharacteristics", &HybridNativeBleNitroSpec::getCharacteristics);
33
34
  prototype.registerHybridMethod("readCharacteristic", &HybridNativeBleNitroSpec::readCharacteristic);
34
35
  prototype.registerHybridMethod("writeCharacteristic", &HybridNativeBleNitroSpec::writeCharacteristic);
35
36
  prototype.registerHybridMethod("subscribeToCharacteristic", &HybridNativeBleNitroSpec::subscribeToCharacteristic);
36
37
  prototype.registerHybridMethod("unsubscribeFromCharacteristic", &HybridNativeBleNitroSpec::unsubscribeFromCharacteristic);
38
+ prototype.registerHybridMethod("isSubscribedToCharacteristic", &HybridNativeBleNitroSpec::isSubscribedToCharacteristic);
37
39
  prototype.registerHybridMethod("requestBluetoothEnable", &HybridNativeBleNitroSpec::requestBluetoothEnable);
38
40
  prototype.registerHybridMethod("state", &HybridNativeBleNitroSpec::state);
39
41
  prototype.registerHybridMethod("subscribeToStateChange", &HybridNativeBleNitroSpec::subscribeToStateChange);
@@ -79,12 +79,14 @@ namespace margelo::nitro::co::zyke::ble {
79
79
  virtual double requestMTU(const std::string& deviceId, double mtu) = 0;
80
80
  virtual void readRSSI(const std::string& deviceId, const std::function<void(bool /* success */, double /* rssi */, const std::string& /* error */)>& callback) = 0;
81
81
  virtual void discoverServices(const std::string& deviceId, const std::function<void(bool /* success */, const std::string& /* error */)>& callback) = 0;
82
+ virtual void discoverServicesWithCharacteristics(const std::string& deviceId, const std::function<void(bool /* success */, const std::string& /* error */)>& callback) = 0;
82
83
  virtual std::vector<std::string> getServices(const std::string& deviceId) = 0;
83
84
  virtual std::vector<std::string> getCharacteristics(const std::string& deviceId, const std::string& serviceId) = 0;
84
85
  virtual void readCharacteristic(const std::string& deviceId, const std::string& serviceId, const std::string& characteristicId, const std::function<void(bool /* success */, const std::shared_ptr<ArrayBuffer>& /* data */, const std::string& /* error */)>& callback) = 0;
85
86
  virtual void writeCharacteristic(const std::string& deviceId, const std::string& serviceId, const std::string& characteristicId, const std::shared_ptr<ArrayBuffer>& data, bool withResponse, const std::function<void(bool /* success */, const std::shared_ptr<ArrayBuffer>& /* responseData */, const std::string& /* error */)>& callback) = 0;
86
87
  virtual void subscribeToCharacteristic(const std::string& deviceId, const std::string& serviceId, const std::string& characteristicId, const std::function<void(const std::string& /* characteristicId */, const std::shared_ptr<ArrayBuffer>& /* data */)>& updateCallback, const std::function<void(bool /* success */, const std::string& /* error */)>& completionCallback) = 0;
87
88
  virtual void unsubscribeFromCharacteristic(const std::string& deviceId, const std::string& serviceId, const std::string& characteristicId, const std::function<void(bool /* success */, const std::string& /* error */)>& callback) = 0;
89
+ virtual bool isSubscribedToCharacteristic(const std::string& deviceId, const std::string& serviceId, const std::string& characteristicId) = 0;
88
90
  virtual void requestBluetoothEnable(const std::function<void(bool /* success */, const std::string& /* error */)>& callback) = 0;
89
91
  virtual BLEState state() = 0;
90
92
  virtual OperationResult subscribeToStateChange(const std::function<void(BLEState /* state */)>& stateCallback) = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-ble-nitro",
3
- "version": "1.10.3",
3
+ "version": "1.12.0",
4
4
  "description": "High-performance React Native BLE library built on Nitro Modules",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -77,25 +77,25 @@
77
77
  "url": "https://github.com/zykeco/react-native-ble-nitro/issues"
78
78
  },
79
79
  "engines": {
80
- "node": ">=16.0.0"
80
+ "node": ">=20.0.0"
81
81
  },
82
82
  "peerDependencies": {
83
83
  "react-native": ">=0.76.0",
84
- "react-native-nitro-modules": ">=0.33.0"
84
+ "react-native-nitro-modules": ">=0.35.0"
85
85
  },
86
86
  "devDependencies": {
87
- "@expo/config-plugins": "^54.0.4",
87
+ "@expo/config-plugins": "^55.0.6",
88
88
  "@types/jest": "^29.5.0",
89
- "@types/node": "^20.19.30",
90
- "@types/react": "^19.2.2",
89
+ "@types/node": "^24.11.0",
90
+ "@types/react": "^19.2.14",
91
91
  "@typescript-eslint/eslint-plugin": "^6.21.0",
92
92
  "@typescript-eslint/parser": "^6.21.0",
93
93
  "eslint": "^8.57.1",
94
- "expo-module-scripts": "^5.0.8",
94
+ "expo-module-scripts": "^55.0.2",
95
95
  "jest": "^29.0.0",
96
- "nitrogen": "^0.33.9",
96
+ "nitrogen": "^0.35.0",
97
97
  "react-native": "^0.83.2",
98
- "react-native-nitro-modules": "^0.33.9",
98
+ "react-native-nitro-modules": "^0.35.0",
99
99
  "rimraf": "^6.1.3",
100
100
  "ts-jest": "^29.4.6",
101
101
  "typescript": "^5.9.3"
@@ -10,12 +10,14 @@ const mockNativeInstance = {
10
10
  requestMTU: jest.fn(),
11
11
  readRSSI: jest.fn(),
12
12
  discoverServices: jest.fn(),
13
+ discoverServicesWithCharacteristics: jest.fn(),
13
14
  getServices: jest.fn(),
14
15
  getCharacteristics: jest.fn(),
15
16
  readCharacteristic: jest.fn(),
16
17
  writeCharacteristic: jest.fn(),
17
18
  subscribeToCharacteristic: jest.fn(),
18
19
  unsubscribeFromCharacteristic: jest.fn(),
20
+ isSubscribedToCharacteristic: jest.fn(),
19
21
  getConnectedDevices: jest.fn(),
20
22
  requestBluetoothEnable: jest.fn(),
21
23
  state: jest.fn(),
@@ -143,6 +145,10 @@ describe('BleNitro', () => {
143
145
  });
144
146
  await BleManager.connect('device-write');
145
147
 
148
+ mockNative.isConnected.mockImplementation((id: string) => {
149
+ return id === 'device-write';
150
+ });
151
+
146
152
  // Mock writeCharacteristic with new signature (success, responseData, error)
147
153
  mockNative.writeCharacteristic.mockImplementation((_deviceId: string, _serviceId: string, _charId: string, _data: ArrayBuffer, withResponse: boolean, callback: (success: boolean, responseData: ArrayBuffer, error: string) => void) => {
148
154
  // For withResponse=false, return empty ArrayBuffer
@@ -171,6 +177,10 @@ describe('BleNitro', () => {
171
177
  });
172
178
  await BleManager.connect('device-write-resp');
173
179
 
180
+ mockNative.isConnected.mockImplementation((id: string) => {
181
+ return id === 'device-write-resp';
182
+ });
183
+
174
184
  // Mock writeCharacteristic to return response data
175
185
  mockNative.writeCharacteristic.mockImplementation((_deviceId: string, _serviceId: string, _charId: string, _data: ArrayBuffer, withResponse: boolean, callback: (success: boolean, responseData: ArrayBuffer, error: string) => void) => {
176
186
  // For withResponse=true, return some response data
@@ -192,12 +202,99 @@ describe('BleNitro', () => {
192
202
  expect(result).toEqual([0xAA, 0xBB, 0xCC]); // Response data as ByteArray
193
203
  });
194
204
 
205
+ test('getServicesWithCharacteristics discovers and returns services with characteristics', async () => {
206
+ // First connect
207
+ mockNative.connect.mockImplementation((id: string, callback: (success: boolean, deviceId: string, error: string) => void, _disconnectCallback?: (deviceId: string, interrupted: boolean, error: string) => void) => {
208
+ callback(true, id, '');
209
+ });
210
+ await BleManager.connect('device-svc');
211
+
212
+ mockNative.isConnected.mockImplementation((id: string) => {
213
+ return id === 'device-svc';
214
+ });
215
+
216
+ // Mock discoverServicesWithCharacteristics
217
+ mockNative.discoverServicesWithCharacteristics.mockImplementation((_deviceId: string, callback: (success: boolean, error: string) => void) => {
218
+ callback(true, '');
219
+ });
220
+
221
+ // Mock getServices to return service UUIDs
222
+ mockNative.getServices.mockReturnValue(['0000180a-0000-1000-8000-00805f9b34fb', '0000180f-0000-1000-8000-00805f9b34fb']);
223
+
224
+ // Mock getCharacteristics per service
225
+ mockNative.getCharacteristics.mockImplementation((_deviceId: string, serviceId: string) => {
226
+ if (serviceId === '0000180a-0000-1000-8000-00805f9b34fb') {
227
+ return ['00002a29-0000-1000-8000-00805f9b34fb'];
228
+ }
229
+ return ['00002a19-0000-1000-8000-00805f9b34fb'];
230
+ });
231
+
232
+ const result = await BleManager.getServicesWithCharacteristics('device-svc');
233
+
234
+ expect(mockNative.discoverServicesWithCharacteristics).toHaveBeenCalledWith(
235
+ 'device-svc',
236
+ expect.any(Function)
237
+ );
238
+ expect(result).toHaveLength(2);
239
+ expect(result[0]).toHaveProperty('uuid');
240
+ expect(result[0]).toHaveProperty('characteristics');
241
+ expect(result[0].characteristics.length).toBeGreaterThan(0);
242
+ });
243
+
244
+ test('getServicesWithCharacteristics rejects on discovery failure', async () => {
245
+ mockNative.connect.mockImplementation((id: string, callback: (success: boolean, deviceId: string, error: string) => void, _disconnectCallback?: (deviceId: string, interrupted: boolean, error: string) => void) => {
246
+ callback(true, id, '');
247
+ });
248
+ await BleManager.connect('device-svc-fail');
249
+
250
+ mockNative.isConnected.mockImplementation((id: string) => {
251
+ return id === 'device-svc-fail';
252
+ });
253
+
254
+ mockNative.discoverServicesWithCharacteristics.mockImplementation((_deviceId: string, callback: (success: boolean, error: string) => void) => {
255
+ callback(false, 'Discovery failed');
256
+ });
257
+
258
+ await expect(BleManager.getServicesWithCharacteristics('device-svc-fail')).rejects.toThrow('Discovery failed');
259
+ });
260
+
261
+ test('getServicesWithCharacteristics rejects when not connected', async () => {
262
+ mockNative.isConnected.mockReturnValue(false);
263
+
264
+ await expect(BleManager.getServicesWithCharacteristics('unknown-device')).rejects.toThrow('Device not connected');
265
+ });
266
+
267
+ test('getServicesWithCharacteristics rejects with BleTimeoutError on timeout', async () => {
268
+ jest.useFakeTimers();
269
+
270
+ mockNative.connect.mockImplementation((id: string, callback: (success: boolean, deviceId: string, error: string) => void) => {
271
+ callback(true, id, '');
272
+ });
273
+ await BleManager.connect('device-timeout');
274
+
275
+ mockNative.isConnected.mockImplementation((id: string) => id === 'device-timeout');
276
+
277
+ // Mock that NEVER calls its callback — simulates hung native discovery
278
+ mockNative.discoverServicesWithCharacteristics.mockImplementation(() => {});
279
+
280
+ const promise = BleManager.getServicesWithCharacteristics('device-timeout');
281
+
282
+ jest.advanceTimersByTime(30_000);
283
+
284
+ await expect(promise).rejects.toThrow('timed out after 30000ms');
285
+
286
+ jest.useRealTimers();
287
+ });
288
+
195
289
  test('readCharacteristic works after connection', async () => {
196
290
  // First connect
197
291
  mockNative.connect.mockImplementation((id: string, callback: (success: boolean, deviceId: string, error: string) => void, _disconnectCallback?: (deviceId: string, interrupted: boolean, error: string) => void) => {
198
292
  callback(true, id, '');
199
293
  });
200
294
  await BleManager.connect('device');
295
+ mockNative.isConnected.mockImplementation((id: string) => {
296
+ return id === 'device';
297
+ });
201
298
 
202
299
  // Then read
203
300
  mockNative.readCharacteristic.mockImplementation((_device: string, _service: string, _char: string, callback: (success: boolean, data: ArrayBuffer, error: string) => void) => {
@@ -239,7 +336,7 @@ describe('BleNitro', () => {
239
336
  const result = await BleManager.disconnect('device');
240
337
 
241
338
  expect(mockNative.disconnect).toHaveBeenCalledWith('device', expect.any(Function));
242
- expect(result).toBe(undefined);
339
+ expect(result).toBe('device');
243
340
  });
244
341
 
245
342
  test('subscribeToCharacteristic calls callback', async () => {
@@ -293,6 +390,47 @@ describe('BleNitro', () => {
293
390
  expect(onDisconnect).toHaveBeenCalledWith(deviceId, true, 'Connection lost');
294
391
  });
295
392
 
393
+ test('isSubscribedToCharacteristic delegates to native with normalized UUIDs', () => {
394
+ mockNative.isSubscribedToCharacteristic.mockReturnValueOnce(true);
395
+
396
+ const result = BleManager.isSubscribedToCharacteristic('device', 'service', 'char');
397
+
398
+ expect(result).toBe(true);
399
+ expect(mockNative.isSubscribedToCharacteristic).toHaveBeenCalledWith(
400
+ 'device',
401
+ '0service-0000-1000-8000-00805f9b34fb',
402
+ '0000char-0000-1000-8000-00805f9b34fb'
403
+ );
404
+ });
405
+
406
+ test('isSubscribedToCharacteristic returns false when not subscribed', () => {
407
+ mockNative.isSubscribedToCharacteristic.mockReturnValueOnce(false);
408
+
409
+ const result = BleManager.isSubscribedToCharacteristic('device', 'service', 'char');
410
+
411
+ expect(result).toBe(false);
412
+ });
413
+
414
+ test('isSubscribedToCharacteristic delegates to native for unknown devices', () => {
415
+ // Native layer handles unknown/disconnected devices by returning false
416
+ mockNative.isSubscribedToCharacteristic.mockReturnValueOnce(false);
417
+
418
+ const result = BleManager.isSubscribedToCharacteristic('unknown-device', 'service', 'char');
419
+
420
+ expect(result).toBe(false);
421
+ expect(mockNative.isSubscribedToCharacteristic).toHaveBeenCalled();
422
+ });
423
+
424
+ test('isSubscribedToCharacteristic propagates native exception', () => {
425
+ mockNative.isSubscribedToCharacteristic.mockImplementationOnce(() => {
426
+ throw new Error('Native error');
427
+ });
428
+
429
+ expect(() =>
430
+ BleManager.isSubscribedToCharacteristic('device', 'service', 'char')
431
+ ).toThrow('Native error');
432
+ });
433
+
296
434
  test('readRSSI requires connected device', async () => {
297
435
  await expect(
298
436
  BleManager.readRSSI('device-not-connected')
@@ -305,6 +443,9 @@ describe('BleNitro', () => {
305
443
  callback(true, id, '');
306
444
  });
307
445
  await BleManager.connect('device-rssi');
446
+ mockNative.isConnected.mockImplementation((id: string) => {
447
+ return id === 'device-rssi';
448
+ });
308
449
 
309
450
  // Mock readRSSI with new signature (success, rssi, error)
310
451
  mockNative.readRSSI.mockImplementation((_deviceId: string, callback: (success: boolean, rssi: number, error: string) => void) => {
@@ -326,6 +467,9 @@ describe('BleNitro', () => {
326
467
  callback(true, id, '');
327
468
  });
328
469
  await BleManager.connect('device-rssi-fail');
470
+ mockNative.isConnected.mockImplementation((id: string) => {
471
+ return id === 'device-rssi-fail';
472
+ });
329
473
 
330
474
  // Mock readRSSI failure
331
475
  mockNative.readRSSI.mockImplementation((_deviceId: string, callback: (success: boolean, rssi: number, error: string) => void) => {
package/src/index.ts CHANGED
@@ -15,6 +15,7 @@ export {
15
15
  BLEState,
16
16
  AndroidScanMode,
17
17
  BleNitroManager,
18
+ BleTimeoutError,
18
19
  } from "./manager";
19
20
 
20
21
  export { BleNitro } from './singleton';