dotintent / react-native-ble-plx

React Native BLE library
Apache License 2.0
3.08k stars 515 forks source link

🐛 The onstatechange was not fired if the app's blue permission was disabled #1218

Closed blood-romantic closed 4 months ago

blood-romantic commented 5 months ago

Prerequisites

Expected Behavior

The onstatechange callback is fired even if the blue permission is disabled

Current Behavior

The onstatechange was not fired if the app's blue permission was disabled

Library version

2.0.3

Device

Iphone14 IOS 16.0.3

Environment info

(Use `node --trace-deprecation ...` to show where the warning was created)
System:
  OS: macOS 14.5
  CPU: (10) arm64 Apple M1 Pro
  Memory: 7.09 GB / 32.00 GB
  Shell:
    version: 3.2.57
    path: /bin/bash
Binaries:
  Node:
    version: 21.5.0
    path: /opt/homebrew/bin/node
  Yarn:
    version: 1.22.17
    path: /usr/local/bin/yarn
  npm:
    version: 10.2.4
    path: /opt/homebrew/bin/npm
  Watchman:
    version: 2023.12.04.00
    path: /opt/homebrew/bin/watchman
Managers:
  CocoaPods:
    version: 1.14.0
    path: /opt/homebrew/lib/ruby/gems/3.3.0/bin/pod
SDKs:
  iOS SDK:
    Platforms:
      - DriverKit 23.5
      - iOS 17.5
      - macOS 14.5
      - tvOS 17.5
      - visionOS 1.2
      - watchOS 10.5
  Android SDK:
    API Levels:
      - "28"
      - "30"
      - "31"
      - "33"
      - "34"
    Build Tools:
      - 30.0.2
      - 30.0.3
      - 31.0.0
      - 33.0.0
      - 33.0.1
      - 34.0.0
    System Images:
      - android-29 | Google APIs ARM 64 v8a
      - android-33 | Google APIs ARM 64 v8a
      - android-33 | Google APIs Intel x86_64 Atom
      - android-33 | Google Play ARM 64 v8a
    Android NDK: Not Found
IDEs:
  Android Studio: 2022.3 AI-223.8836.35.2231.10406996
  Xcode:
    version: 15.4/15F31d
    path: /usr/bin/xcodebuild
Languages:
  Java:
    version: 17.0.9
    path: /usr/bin/javac
  Ruby:
    version: 3.3.0
    path: /opt/homebrew/opt/ruby/bin/ruby
npmPackages:
  "@react-native-community/cli": Not Found
  react:
    installed: 18.2.0
    wanted: 18.2.0
  react-native:
    installed: 0.73.3
    wanted: 0.73.3
  react-native-macos: Not Found
npmGlobalPackages:
  "*react-native*": Not Found
Android:
  hermesEnabled: Not found
  newArchEnabled: Not found
iOS:
  hermesEnabled: Not found
  newArchEnabled: false

Steps to reproduce

  1. install the App and disallow the Blue Tooth permission
  2. swipe out the app and toggle the phone's Blue Tooth
  3. go back to the app and the onstatechange is not fired

Formatted code sample or link to a repository

async componentDidMount() {
    ble.onStateChange(state => {
      console.log('__BLE-STATE__', state);
      if (state === 'PoweredOn') {
        this.setState({
          bleState: BLE_STATE.POWERED_ON,
        });
      } else if (state === 'PoweredOff') {
        this.setState({
          bleState: BLE_STATE.POWERED_OFF,
        });
      } else if (
        Platform.OS === 'android' &&
        state !== 'PoweredOn' &&
        state !== 'PoweredOff'
      ) {
        this.setState({grantedBluetooth: false});
      }
    }, true);
}

Relevant log output

[javascript] '__BLE-STATE__', 'Unknown'

[javascript] '__BLE-STATE__', 'Unauthorized'

Additional information

No response

intent-kacper-cyranowski commented 5 months ago

Hi @blood-romantic, can you share the whole class component? I want to know:

  1. How do you refresh it?
  2. Do you check permission status of mounting the component or only on state change?

From what I checked, if you change permissions when the app is on the background this listener won't trigger the event - it will only work if the app is in the foreground.

blood-romantic commented 5 months ago

@intent-kacper-cyranowski

If I disallow the blue tooth permission on the app, the onstatechange will never be triggered whatever the blue tooth on iPhone is toggled to PowerOn or PowerOff

blood-romantic commented 5 months ago
import PropTypes from 'prop-types';
import MicroPaymentsAPI from '../MicroPaymentsAPI/index';
import Icon from 'react-native-vector-icons/MaterialIcons';
import Spinner from 'react-native-spinkit';
import {ble} from '../BLEManager/index';
import {scanForMachines} from '../BLEManager/discoveryManager';
import {
  scanningMessages as scanMsg,
  cardMessages,
  loginMessages,
  noRoomsMessages,
  genericErrorMessage,
  serverDownMessages,
  sessionMessages,
  normalMessages,
} from '../constants/copy';
import STYLES, {COLORS, navStyling} from '../styles/index';
import {BLE_STATE} from '../BLEManager/discoveryManager';
import {
  clearTimer,

  checkCurrentSession,
} from '../utilities';
import {showSupportRequestAlert} from '../navUtils';
// import {logCustom} from '../fabricUtils';
import {
  captureException,

} from '../sentryUtils';
import PushNotification from 'react-native-push-notification';
import WC from '../WaveController';
// import {logFirebaseEvent} from '../loggingUtils';
import Rate, {AndroidMarket} from 'react-native-rate';
// import {captureFirebaseEvent} from '../firebaseUtils';
// import perf from '@react-native-firebase/perf';
import {connect} from 'react-redux';
import {
  getUser,
  getUserData,
  getUserLocationData,
  logout,
  updateUserData,
  updateUserBalance,
  getUserRooms,
  updateUserRoom,
  updateUserLocation,
  resetDifferentUser,
} from '../redux/reducers/user';
import TCPError from '../MicroPaymentsAPI/tcpError';
import * as CONSTANTS from '../constants/ble';
import {SCREENS} from '../Chores/Routes';

/*
    The screen that manages a displayed `MachineList` component, the user's room selection,
    scanning lifecycle for machines, and the states of the bluetooth status, scanning status,
    and room (controller ID) filter status.
*/
class ScanningScreen extends Component {
  static propTypes = {
    componentId: PropTypes.node,
    getUser: PropTypes.func,
    getUserData: PropTypes.func,
    logout: PropTypes.func,
    updateUserData: PropTypes.func,
    session: PropTypes.object,
    notificationsOn: PropTypes.bool,
    carrierId: PropTypes.string,
    userName: PropTypes.string,
    deviceModel: PropTypes.string,
    controllerId: PropTypes.string,
    availableRooms: PropTypes.array,
    selectedRoom: PropTypes.string,
  };

  static options = {
    statusBar: {
      backgroundColor: COLORS.BLUE,
      style: 'light',
    },
  };

  constructor(props) {
    super(props);
    this.AppStateSubscription = null;
    this.NavigationFocusEventSubScription = null;
    this.NavigationBlurEventSubScription = null;
    this.backHandler = null;
    this.bleStateHandler = null;
    this.state = {
      bleState: BLE_STATE.POWERED_OFF,
    };
    this.trace = null;
    this.needRetryGetRooms = true;
    this.needRetryGetInfo = true;
  }

  // --------- LIFE CYCLE METHODS ---------------
  // ********************************************

  _handleBackPress = () => {
    BackHandler.exitApp();
  };

  async componentDidMount() {
    this.bleStateHandler = ble.onStateChange(state => {
      console.log('__BLE-STATE__', state);
      if (state === 'PoweredOn') {
        this.setState({
          bleState: BLE_STATE.POWERED_ON,
        });
      } else if (state === 'PoweredOff') {
        this.setState({
          bleState: BLE_STATE.POWERED_OFF,
        });
      } else if (
        Platform.OS === 'android' &&
        state !== 'PoweredOn' &&
        state !== 'PoweredOff'
      ) {
        this.setState({grantedBluetooth: false});
      }
    }, true);
    // Step 7: Set app event listener for app changes
  }

  async componentWillUnmount() {
    this.AppStateSubscription?.remove();
    this.NavigationFocusEventSubScription?.remove();
    this.NavigationBlurEventSubScription?.remove();
    this.bleStateHandler?.remove();
    clearTimer(this);
    await this.trace.stop();
  }

  render() {
    ...
  }

const mapStateToProps = state => {
  return {
    session: state.userReducer.session,
    user: state.userReducer.user,
    mfaAuthenticated: state.userReducer.mfaAuthenticated,
    appUserId: state.userReducer.appUserId,
    notificationValue: state.userReducer.notificaitonValue,
    notificationText: state.userReducer.notificationText,
    mfaSystemAvailable: state.userReducer.mfaSystemAvailable,
    mfaUserEnrolled: state.userReducer.mfaUserEnrolled,
    mfaSessionValid: state.userReducer.mfaSessionValid,
    mfaRequiredFlag: state.userReducer.mfaRequiredFlag,
    mfaUserId: state.userReducer.mfaUserId,
    mfaEmailFactorId: state.userReducer.mfaEmailFactorId,
    mfaEmail: state.userReducer.Email,
    mfaPhoneFactorId: state.userReducer.mfaPhoneFactorId,
    mfaPhone: state.userReducer.mfaPhone,
    mfaPhoneStatus: state.userReducer.mfaPhoneStatus,
    mfaEmailStatus: state.userReducer.mfaEmailStatus,
    notificationsOn: state.userReducer.notificationsOn,
    carrierId: state.userReducer.carrierId,
    userName: state.userReducer.userName,
    waveController: state.userReducer.waveController,
    existLocationData: state.userReducer.existLocationData,
    locationId: state.userReducer.locationId,
    deviceModel: state.appReducer.deviceModel,
    availableRooms: state.userReducer.availableRooms,
    selectedRoom: state.userReducer.selectedRoom,
    controllerId: state.userReducer.controllerId,
  };
};

const mapDispatchToProps = {
  getUser,
  getUserData,
  getUserLocationData,
  logout,
  updateUserData,
  updateUserBalance,
  getUserRooms,
  updateUserRoom,
  updateUserLocation,
  resetDifferentUser,
};

export default connect(mapStateToProps, mapDispatchToProps)(ScanningScreen);
intent-kacper-cyranowski commented 4 months ago

As I mentioned, while you are in settings, your app is in background. It’s not possible to handle events in background as the app execution is stopped. If you want to handle this case you could use AppState and manually read the state when app enters the foreground

intent-kacper-cyranowski commented 4 months ago

Hi @blood-romantic Do you still need help with the issue?