@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
136 lines • 7.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.useBleDevicesScanning = void 0;
const react_1 = require("react");
const rxjs_1 = require("rxjs");
const operators_1 = require("rxjs/operators");
const devices_1 = require("@ledgerhq/devices");
const errors_1 = require("@ledgerhq/errors");
const logs_1 = require("@ledgerhq/logs");
const DEFAULT_DEVICE_NAME = "Device";
const DEFAULT_RESTART_SCANNING_TIMEOUT_MS = 4000;
/**
* Scans the BLE devices around the user
*
* Warning: if a communication is started with a device, the scanning should be stopped
*
* Warning: handling of bluetooth (and location for Android) permissions and enabling bluetooth (and location) services are not handled here.
* They should be handled (with fallback logic) by the consumer of this hook.
*
* Reason: depending on the bleTransportListen function and the user's operating system, errors related to denied bluetooth (and location for Android) permissions
* or related to disabled bluetooth (and location) services might be different.
* For ex:
* - on Android, using the current Transport from react-native-hw-transport-ble, if the bluetooth is off,
* a BluetoothScanStartFailed error is thrown, not an actual "BluetoothOff" or "BluetoothUnauthorized" error.
* It is a problem because this BluetoothScanStartFailed error could happen for other reason than the BLE being off.
* On the other side, if the location service (needed for Android) is off, an error "LocationServicesDisabled" is thrown.
*
* - on iOS, using the current Transport from react-native-hw-transport-ble, if the bluetooth is off, no error is thrown at all.
*
* @param bleTransportListen The listen function from an implementation of a BLE transport
* @param filterByDeviceModelIds An array of device model ids to filter on
* @param filterOutDevicesByDeviceIds An array of device ids to filter out
* @param stopBleScanning Flag to stop or continue the scanning
* @param restartScanningTimeoutMs When a restart is needed (on some specific errors, or for the first restart
* that makes the scanning more resilient to a previously paired device with which a communication was happening),
* time in ms after which the restart is actually happening
* @param enabled flag to enable the hook
* @returns An object containing:
* - scannedDevices: list of ScannedDevice found by the scanning
* - scanningBleError: if an error occurred, a BleError, otherwise null
*/
const useBleDevicesScanning = ({ bleTransportListen, stopBleScanning, filterByDeviceModelIds, filterOutDevicesByDeviceIds, restartScanningTimeoutMs = DEFAULT_RESTART_SCANNING_TIMEOUT_MS, enabled = true, }) => {
const [scanningBleError, setScanningBleError] = (0, react_1.useState)(null);
const [scannedDevices, setScannedDevices] = (0, react_1.useState)([]);
// To check for duplicates. The ref will persist for the full lifetime of the component
// in which the hook is called, and does not re-trigger the hook when being updated.
const scannedDevicesRef = (0, react_1.useRef)([]);
// To stop, call the unsubscribe and cleaning function, and re-run the scanning
const [restartScanningNonce, setRestartScanningNonce] = (0, react_1.useState)(0);
// To request a restart of the scanning
const [isRestartNeeded, setIsRestartNeeded] = (0, react_1.useState)(true);
(0, react_1.useEffect)(() => {
if (stopBleScanning || !enabled) {
return;
}
const bleScanningSource = new rxjs_1.Observable(bleTransportListen);
// Concat to flatten the events emitted by the scanning, that could arrive too fast
const sub = (0, rxjs_1.of)(bleScanningSource)
.pipe((0, operators_1.concatAll)())
.subscribe({
next: (event) => {
setScanningBleError(null);
const { type, descriptor } = event;
if (type === "add" && descriptor) {
const transportDevice = descriptor;
const isScannedDeviceDuplicate = scannedDevicesRef.current.some(d => d.deviceId === transportDevice.id);
// Avoiding duplicates
if (isScannedDeviceDuplicate) {
return;
}
const shouldScannedDeviceBeFilteredOut = filterOutDevicesByDeviceIds?.some(deviceId => deviceId === transportDevice.id);
if (shouldScannedDeviceBeFilteredOut) {
return;
}
if (transportDevice.serviceUUIDs && transportDevice.serviceUUIDs.length > 0) {
const bleInfo = (0, devices_1.getInfosForServiceUuid)(transportDevice.serviceUUIDs[0]);
if (!bleInfo) {
return;
}
// Filters on the model ids, if asked
if (filterByDeviceModelIds &&
!filterByDeviceModelIds.includes(bleInfo.deviceModel.id)) {
return;
}
const newScannedDevice = {
deviceModel: bleInfo.deviceModel,
deviceName: transportDevice.localName ?? transportDevice.name ?? DEFAULT_DEVICE_NAME,
deviceId: transportDevice.id,
bleRssi: transportDevice.rssi,
};
setScannedDevices(scannedDevices => [...scannedDevices, newScannedDevice]);
scannedDevicesRef.current.push(newScannedDevice);
}
}
},
error: (error) => {
(0, logs_1.log)("useBleDevicesScanning:error", `${error.type}: ${error.message}`);
if (error instanceof errors_1.HwTransportError &&
error.type === errors_1.HwTransportErrorType.BluetoothScanStartFailed) {
setIsRestartNeeded(true);
}
setScanningBleError(error);
},
});
return () => {
sub.unsubscribe();
};
}, [
restartScanningNonce,
bleTransportListen,
stopBleScanning,
filterByDeviceModelIds,
filterOutDevicesByDeviceIds,
enabled,
]);
// Triggers after a defined time a restart of the scanning if needed
(0, react_1.useEffect)(() => {
let timer;
if (isRestartNeeded && !stopBleScanning) {
timer = setTimeout(() => {
setRestartScanningNonce(prev => prev + 1);
setIsRestartNeeded(false);
}, restartScanningTimeoutMs);
}
return () => {
if (timer)
clearTimeout(timer);
};
}, [isRestartNeeded, restartScanningTimeoutMs, stopBleScanning]);
return {
scannedDevices,
scanningBleError,
};
};
exports.useBleDevicesScanning = useBleDevicesScanning;
//# sourceMappingURL=useBleDevicesScanning.js.map