react-native-ble-plx icon indicating copy to clipboard operation
react-native-ble-plx copied to clipboard

fix: comprehensive React Native 0.80.0+ compatibility fix

Open sweetbot opened this issue 2 months ago • 7 comments

Problem

React Native 0.80.0 was the first release to include the PromiseImpl null-safety changes.

The React Native Commit in April 2025 "fix nullsafe FIXMEs for PromiseImpl.java and mark nullsafe" by Gijs Weterings 4c8ea858a53 - fix nullsafe FIXMEs for PromiseImpl.java and mark nullsafe

What This Means: React Native 0.80.0+ enforces non-null error codes in PromiseImpl.reject() React Native 0.79.x and earlier allowed null as the error code The change added @Nullsafe(Nullsafe.Mode.LOCAL) annotation to PromiseImpl class This triggers NullPointerException when null is passed as the error code

Root Cause

React Native 0.80.0+ added @Nullsafe(Nullsafe.Mode.LOCAL) annotation to PromiseImpl class, which enforces non-null error codes at runtime.

This bluetooth library was calling in a lot of places:

safePromise.reject(null, errorConverter.toJs(error));

But React Native 0.80.0+ now requires:

promise.reject(String code, Object error); // code cannot be null

React Native 0.80.0 was the first release to include this change that broke some features of this library

Impact

It fixes an urgent compatibility issue impacting a wide range of users forced to move to React Native 0.81.4+ (especially Expo SDK 54+ projects). Updating to Expo 54 makes it impossible to use this library without crash, since Expo 54 is only compatible with React Native 0.81.4+.

This will fix issues for Ledger reactive native Device Management SDK and other reactive native SDKs which use this library as the transport layer.

Related Issue that I reported 2 days ago: #1310 - Android crash in BLETransport APIs when React Native version is 0.81.4

Solution

Replace all null error codes with proper BleErrorCode enum values using error.errorCode.name().

  • Fix all instances of null error codes in promise.reject calls
  • Include both safePromise.reject and promise.reject fixes
  • Address React Native 0.80.0+ null-safety requirements

Java code changes in this PR

In android/src/main/java/com/bleplx/BlePlxModule.java

  • Replace safePromise.reject(null, ...) with safePromise.reject(error.errorCode.name(), ...)
  • Replace promise.reject(null, ...) with promise.reject(error.errorCode.name(), ...)
  • Replace promise.reject(null, ...) with promise.reject(bleError.errorCode.name(), ...)

Testing results

  • ✅ Works with React Native 0.79.6
  • ✅ Works with React Native 0.81.4+
  • ✅ No more crashes on disconnect operations
  • ✅ Maintains backward compatibility

Error Log

Some more details of Native Android logs here:

09:44:45.917  3915-4176  ReactNativeJS           com.ranitomeya.ledgerexpodemo        I  🔌 Starting Manual disconnect - setting disconnecting state...
2025-10-26 09:44:45.918  3915-4176  ReactNativeJS           com.ranitomeya.ledgerexpodemo        I  Manual disconnect - disconnecting state set successfully
2025-10-26 09:44:45.918  3915-4176  ReactNativeJS           com.ranitomeya.ledgerexpodemo        I  Manual disconnect - disconnecting...
2025-10-26 09:44:45.933  3915-4176  ReactNativeJS           com.ranitomeya.ledgerexpodemo        I  Manual disconnect - session validated, proceeding with disconnect
2025-10-26 09:44:45.935  3915-4176  ReactNativeJS           com.ranitomeya.ledgerexpodemo        I  Manual disconnect - calling dmk.disconnect with sessionId: 5e0cb30d-13fa-49ad-bde2-225fd04639f6
2025-10-26 09:44:45.941  3915-4176  ReactNativeJS           com.ranitomeya.ledgerexpodemo        I  Manual disconnect - dmk.disconnect completed successfully
2025-10-26 09:44:45.952  3915-4542  BluetoothGatt           com.ranitomeya.ledgerexpodemo        D  cancelOpen() - device: XX:XX:XX:XX:76:22
2025-10-26 09:44:45.956  3915-3927  BluetoothGatt           com.ranitomeya.ledgerexpodemo        D  onClientConnectionState() - status=0 connected=false device=XX:XX:XX:XX:76:22
2025-10-26 09:44:45.956  3915-3927  BluetoothGatt           com.ranitomeya.ledgerexpodemo        D  unregisterApp()
2025-10-26 09:44:45.958  3915-3927  BluetoothGatt           com.ranitomeya.ledgerexpodemo        D  setCharacteristicNotification() - uuid: 13d63400-2c97-6004-0001-4c6564676572 enable: false
2025-10-26 09:44:45.960  3915-4542  BluetoothGatt           com.ranitomeya.ledgerexpodemo        D  close()
2025-10-26 09:44:45.963  3915-4670  AndroidRuntime          com.ranitomeya.ledgerexpodemo        E  FATAL EXCEPTION: RxComputationThreadPool-4 
                                                                                                    Process: com.ranitomeya.ledgerexpodemo, PID: 3915
                                                                                                    io.reactivex.exceptions.CompositeException: 2 exceptions occurred. 
                                                                                               	at io.reactivex.internal.subscribers.LambdaSubscriber.onError(LambdaSubscriber.java:82)
                                                                                                    	at io.reactivex.internal.operators.flowable.FlowableDoOnEach$DoOnEachSubscriber.onError(FlowableDoOnEach.java:111)
                                                                                                    	at io.reactivex.internal.operators.flowable.FlowableDoOnLifecycle$SubscriptionLambdaSubscriber.onError(FlowableDoOnLifecycle.java:85)
                                                                                                    	at io.reactivex.internal.operators.flowable.FlowableObserveOn$BaseObserveOnSubscriber.checkTerminated(FlowableObserveOn.java:209)
                                                                                                    	at io.reactivex.internal.operators.flowable.FlowableObserveOn$ObserveOnSubscriber.runAsync(FlowableObserveOn.java:399)
                                                                                                    	at io.reactivex.internal.operators.flowable.FlowableObserveOn$BaseObserveOnSubscriber.run(FlowableObserveOn.java:176)
                                                                                                    	at io.reactivex.internal.schedulers.ScheduledRunnable.run(ScheduledRunnable.java:66)
                                                                                                    	at io.reactivex.internal.schedulers.ScheduledRunnable.call(ScheduledRunnable.java:57)
                                                                                                    	at java.util.concurrent.FutureTask.run(FutureTask.java:317)
                                                                                                    	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:348)
                                                                                                    	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1156)
                                                                                                    	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:651)
                                                                                                    	at java.lang.Thread.run(Thread.java:1119)
                                                                                                      ComposedException 1 :
                                                                                                    	com.polidea.rxandroidble2.exceptions.BleDisconnectedException: Disconnected from MAC='XX:XX:XX:XX:XX:XX' with status 0 (GATT_SUCCESS)
                                                                                                    		at com.polidea.rxandroidble2.internal.connection.RxBleGattCallback$2.onConnectionStateChange(RxBleGattCallback.java:81)
                                                                                                    		at android.bluetooth.BluetoothGatt$GattCallback.lambda$onClientConnectionState$3(BluetoothGatt.java:426)
                                                                                                    		at android.bluetooth.BluetoothGatt$GattCallback.$r8$lambda$g0wdVhK6mBSTzAIOY21zUwHI--c(Unknown Source:0)
                                                                                                    		at android.bluetooth.BluetoothGatt$GattCallback$$ExternalSyntheticLambda10.run(D8$$SyntheticClass:0)
                                                                                                    		at android.bluetooth.BluetoothGatt$GattCallback.runOrQueueCallback(BluetoothGatt.java:277)
                                                                                                    		at android.bluetooth.BluetoothGatt$GattCallback.onClientConnectionState(BluetoothGatt.java:422)
                                                                                                    		at android.bluetooth.IBluetoothGattCallback$Stub.onTransact(IBluetoothGattCallback.java:209)
                                                                                                    		at android.os.Binder.execTransactInternal(Binder.java:1426)
                                                                                                    		at android.os.Binder.execTransact(Binder.java:1365)
                                                                                                    	Caused by: java.lang.NullPointerException: Parameter specified as non-null is null: method com.facebook.react.bridge.PromiseImpl.reject, parameter code
                                                                                                    		at com.facebook.react.bridge.PromiseImpl.reject(Unknown Source:2)
                                                                                                    		at com.bleplx.utils.SafePromise.reject(SafePromise.java:25)
                                                                                                    		at com.bleplx.BlePlxModule$41.onError(BlePlxModule.java:817)
                                                                                                    		at com.bleplx.adapter.utils.SafeExecutor.error(SafeExecutor.java:30)
                                                                                                    		at com.bleplx.adapter.BleModule.lambda$safeMonitorCharacteristicForDevice$45(BleModule.java:1485)
                                                                                                    		at com.bleplx.adapter.BleModule.$r8$lambda$JVuqIGnSfaxzZFLoyHm91xQUhdI(Unknown Source:0)
                                                                                                    		at com.bleplx.adapter.BleModule$$ExternalSyntheticLambda3.accept(D8$$SyntheticClass:0)
                                                                                                    		at io.reactivex.internal.subscribers.LambdaSubscriber.onError(LambdaSubscriber.java:79)
                                                                                                    		at io.reactivex.internal.operators.flowable.FlowableDoOnEach$DoOnEachSubscriber.onError(FlowableDoOnEach.java:111)
                                                                                                    		at io.reactivex.internal.operators.flowable.FlowableDoOnLifecycle$SubscriptionLambdaSubscriber.onError(FlowableDoOnLifecycle.java:85)
                                                                                                    		at io.reactivex.internal.operators.flowable.FlowableObserveOn$BaseObserveOnSubscriber.checkTerminated(FlowableObserveOn.java:209)
                                                                                                    		at io.reactivex.internal.operators.flowable.FlowableObserveOn$ObserveOnSubscriber.runAsync(FlowableObserveOn.java:399)
                                                                                                    		at io.reactivex.internal.operators.flowable.FlowableObserveOn$BaseObserveOnSubscriber.run(FlowableObserveOn.java:176)
                                                                                                    		at io.reactivex.internal.schedulers.ScheduledRunnable.run(ScheduledRunnable.java:66)
                                                                                                    		at io.reactivex.internal.schedulers.ScheduledRunnable.call(ScheduledRunnable.java:57)


E  		at java.util.concurrent.FutureTask.run(FutureTask.java:317) 
                                                                              		at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:348)
                                                                                                    		at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1156)
                                                                                                    		at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:651)
                                                                                                    		at java.lang.Thread.run(Thread.java:1119)
                                                                                                      ComposedException 2 :
                                                                                                    	java.lang.NullPointerException: Parameter specified as non-null is null: method com.facebook.react.bridge.PromiseImpl.reject, parameter code
                                                                                                    		at com.facebook.react.bridge.PromiseImpl.reject(Unknown Source:2)
                                                                                                    		at com.bleplx.utils.SafePromise.reject(SafePromise.java:25)
                                                                                                    		at com.bleplx.BlePlxModule$41.onError(BlePlxModule.java:817)
                                                                                                    		at com.bleplx.adapter.utils.SafeExecutor.error(SafeExecutor.java:30)
                                                                                                    		at com.bleplx.adapter.BleModule.lambda$safeMonitorCharacteristicForDevice$45(BleModule.java:1485)
                                                                                                    		at com.bleplx.adapter.BleModule.$r8$lambda$JVuqIGnSfaxzZFLoyHm91xQUhdI(Unknown Source:0)
                                                                                                    		at com.bleplx.adapter.BleModule$$ExternalSyntheticLambda3.accept(D8$$SyntheticClass:0)
                                                                                                    		at io.reactivex.internal.subscribers.LambdaSubscriber.onError(LambdaSubscriber.java:79)
                                                                                                    		at io.reactivex.internal.operators.flowable.FlowableDoOnEach$DoOnEachSubscriber.onError(FlowableDoOnEach.java:111)
                                                                                                    		at io.reactivex.internal.operators.flowable.FlowableDoOnLifecycle$SubscriptionLambdaSubscriber.onError(FlowableDoOnLifecycle.java:85)
                                                                                                    		at io.reactivex.internal.operators.flowable.FlowableObserveOn$BaseObserveOnSubscriber.checkTerminated(FlowableObserveOn.java:209)
                                                                                                    		at io.reactivex.internal.operators.flowable.FlowableObserveOn$ObserveOnSubscriber.runAsync(FlowableObserveOn.java:399)
                                                                                                    		at io.reactivex.internal.operators.flowable.FlowableObserveOn$BaseObserveOnSubscriber.run(FlowableObserveOn.java:176)
                                                                                                    		at io.reactivex.internal.schedulers.ScheduledRunnable.run(ScheduledRunnable.java:66)
                                                                                                    		at io.reactivex.internal.schedulers.ScheduledRunnable.call(ScheduledRunnable.java:57)
                                                                                                    		at java.util.concurrent.FutureTask.run(FutureTask.java:317)
                                                                                                    		at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:348)
                                                                                                    		at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1156)
                                                                                                    		at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:651)
                                                                                                    		at java.lang.Thread.run(Thread.java:1119)
                                                                                                    

sweetbot avatar Oct 26 '25 19:10 sweetbot

Hopefully this gets pulled in soon, I used patch-package to fix the current version.

Thanks for making the pull request :)

Yusuf-Munir avatar Oct 29 '25 08:10 Yusuf-Munir

After upgrading our app to Expo 54 we ran into this issue; and I can confirm the suggested patch resolved it for us.

edwinw6 avatar Oct 30 '25 16:10 edwinw6

This patch fixed the issue for us also.

Would be great if this could be merged to the official library.

alariois avatar Nov 11 '25 12:11 alariois

It would be good to look at existing open issues (#1303) and PRs (#1304) in the future so you don't replicate other peoples efforts.

b1naryth1ef avatar Nov 18 '25 17:11 b1naryth1ef

Any updates? We really stuck on our update to react native 0.80.0 / expo54

ArtemFokin avatar Nov 18 '25 18:11 ArtemFokin

I hate to say it but I did a fork of this library because I had to fix the same problem... I just reviewed your PR and it's identical to one of the problem I fixed @ArtemFokin @b1naryth1ef @alariois

If you guys want to collaborate.

https://github.com/sfourdrinier/react-native-ble-plx

sfourdrinier avatar Nov 22 '25 18:11 sfourdrinier

@sfourdrinier I tried your fork and cant find the types (like BleManager) in code, import doesnt work, you should give a check.

Master-Antonio avatar Dec 09 '25 09:12 Master-Antonio