From dc449557851cd0a2db3dfde06836584c770a3305 Mon Sep 17 00:00:00 2001 From: Peter Sanderson Date: Fri, 14 Feb 2025 15:39:59 +0100 Subject: [PATCH] feat: add Bluetooth common reducers and actions feat: add Bluetooth common reducers and actions fix: make typings for storageLoad a bit more 'nice' --- packages/connect/src/device/Device.ts | 2 +- packages/connect/src/types/device.ts | 2 +- packages/transport/src/types/index.ts | 2 +- suite-common/bluetooth/package.json | 17 +++ .../bluetooth/src/bluetoothActions.ts | 45 ++++++ .../bluetooth/src/bluetoothReducerCreator.ts | 133 ++++++++++++++++++ suite-common/bluetooth/tsconfig.json | 9 ++ yarn.lock | 10 ++ 8 files changed, 217 insertions(+), 3 deletions(-) create mode 100644 suite-common/bluetooth/package.json create mode 100644 suite-common/bluetooth/src/bluetoothActions.ts create mode 100644 suite-common/bluetooth/src/bluetoothReducerCreator.ts create mode 100644 suite-common/bluetooth/tsconfig.json diff --git a/packages/connect/src/device/Device.ts b/packages/connect/src/device/Device.ts index a4023435ef42..fb2b6e1adbf9 100644 --- a/packages/connect/src/device/Device.ts +++ b/packages/connect/src/device/Device.ts @@ -221,7 +221,7 @@ export class Device extends TypedEmitter { this.transportPath = descriptor.path; this.transportSessionOwner = descriptor.sessionOwner; this.transportDescriptorType = descriptor.type; - this.bluetoothProps = descriptor.uuid ? { uuid: descriptor.uuid } : undefined; + this.bluetoothProps = descriptor.id ? { id: descriptor.id } : undefined; this.session = descriptor.session; this.lastAcquiredHere = false; diff --git a/packages/connect/src/types/device.ts b/packages/connect/src/types/device.ts index e854559202f1..52ad4107c041 100644 --- a/packages/connect/src/types/device.ts +++ b/packages/connect/src/types/device.ts @@ -82,7 +82,7 @@ type BaseDevice = { }; export type BluetoothDeviceProps = { - uuid: string; + id: string; }; export type KnownDevice = BaseDevice & { diff --git a/packages/transport/src/types/index.ts b/packages/transport/src/types/index.ts index 79131b51ba02..2b24b04e615a 100644 --- a/packages/transport/src/types/index.ts +++ b/packages/transport/src/types/index.ts @@ -29,7 +29,7 @@ export type Descriptor = Omit & { /** only reported by old bridge */ debug?: boolean; /** only reported by transport-bluetooth */ - uuid?: string; + id?: string; }; export interface Logger { diff --git a/suite-common/bluetooth/package.json b/suite-common/bluetooth/package.json new file mode 100644 index 000000000000..78accc4a9b8c --- /dev/null +++ b/suite-common/bluetooth/package.json @@ -0,0 +1,17 @@ +{ + "name": "@suite-common/bluetooth", + "version": "1.0.0", + "private": true, + "license": "See LICENSE.md in repo root", + "sideEffects": false, + "main": "src/index", + "scripts": { + "depcheck": "yarn g:depcheck", + "type-check": "yarn g:tsc --build" + }, + "dependencies": { + "@reduxjs/toolkit": "1.9.5", + "@suite-common/redux-utils": "workspace:*", + "@suite-common/wallet-core": "workspace:*" + } +} diff --git a/suite-common/bluetooth/src/bluetoothActions.ts b/suite-common/bluetooth/src/bluetoothActions.ts new file mode 100644 index 000000000000..7ce98d33ee6a --- /dev/null +++ b/suite-common/bluetooth/src/bluetoothActions.ts @@ -0,0 +1,45 @@ +import { createAction } from '@reduxjs/toolkit'; + +import { + BluetoothDevice, + BluetoothScanStatus, + DeviceBluetoothStatus, +} from './bluetoothReducerCreator'; + +export const BLUETOOTH_PREFIX = '@suite/bluetooth'; + +export const bluetoothAdapterEventAction = createAction( + `${BLUETOOTH_PREFIX}/adapter-event`, + ({ isPowered }: { isPowered: boolean }) => ({ payload: { isPowered } }), +); + +type BluetoothDeviceListUpdatePayload = { + devices: BluetoothDevice[]; + knownDevices: BluetoothDevice[]; +}; + +export const bluetoothDeviceListUpdate = createAction( + `${BLUETOOTH_PREFIX}/device-list-update`, + ({ devices, knownDevices }: BluetoothDeviceListUpdatePayload) => ({ + payload: { devices, knownDevices }, + }), +); + +export const bluetoothConnectDeviceEventAction = createAction( + `${BLUETOOTH_PREFIX}/device-connection-status`, + ({ connectionStatus, id }: { id: string; connectionStatus: DeviceBluetoothStatus }) => ({ + payload: { id, connectionStatus }, + }), +); + +export const bluetoothScanStatusAction = createAction( + `${BLUETOOTH_PREFIX}/scan-status`, + ({ status }: { status: BluetoothScanStatus }) => ({ payload: { status } }), +); + +export const allBluetoothActions = { + bluetoothAdapterEventAction, + bluetoothDeviceListUpdate, + bluetoothConnectDeviceEventAction, + bluetoothScanStatusAction, +}; diff --git a/suite-common/bluetooth/src/bluetoothReducerCreator.ts b/suite-common/bluetooth/src/bluetoothReducerCreator.ts new file mode 100644 index 000000000000..a7af65a68977 --- /dev/null +++ b/suite-common/bluetooth/src/bluetoothReducerCreator.ts @@ -0,0 +1,133 @@ +import { AnyAction, Draft } from '@reduxjs/toolkit'; + +import { createReducerWithExtraDeps } from '@suite-common/redux-utils'; +import { deviceActions } from '@suite-common/wallet-core'; + +import { + bluetoothAdapterEventAction, + bluetoothConnectDeviceEventAction, + bluetoothDeviceListUpdate, + bluetoothScanStatusAction, +} from './bluetoothActions'; + +export type BluetoothScanStatus = 'running' | 'done' | 'error'; + +export type BluetoothDevice = { + id: string; + name: string; + data: number[]; + lastUpdate: number; + status: DeviceBluetoothStatus | null; +}; + +export type DeviceBluetoothStatus = + | { type: 'pairing'; pin?: string } + | { type: 'paired' } + | { type: 'connecting' } + | { type: 'connected' } + | { + type: 'error'; + error: string; + }; + +export type DeviceBluetoothStatusType = DeviceBluetoothStatus['type']; + +type BluetoothState = { + isAdapterEnabled: boolean; + scanStatus: BluetoothScanStatus; + + // This will be persisted, those are devices we believed that are paired + // (because we already successfully paired them in the Suite) in the Operating System + knownDevices: T[]; + + // This list of devices that is union of saved-devices and device that we get from scan + devices: T[]; +}; + +export const bluetoothReducerCreator = () => { + const initialState: BluetoothState = { + isAdapterEnabled: true, // To prevent the UI from flickering when the page is loaded + scanStatus: 'running', // To prevent the UI from flickering when the page is loaded + knownDevices: [] as T[], + devices: [] as T[], + }; + + return createReducerWithExtraDeps>(initialState, (builder, extra) => + builder + .addCase(bluetoothAdapterEventAction, (state, { payload: { isPowered } }) => { + state.isAdapterEnabled = isPowered; + if (!isPowered) { + state.devices = []; + state.scanStatus = 'done'; + } + }) + .addCase(bluetoothDeviceListUpdate, (state, { payload: { devices, knownDevices } }) => { + const newList = new Map(); + knownDevices.forEach(device => { + newList.set(device.id, device as T); + }); + + state.knownDevices = knownDevices as Draft[]; + + devices.forEach(device => { + newList.set(device.id, device as T); + }); + + state.devices = Array.from(newList.values()).sort( + (a, b) => a.lastUpdate - b.lastUpdate, + ) as Draft[]; + }) + .addCase( + bluetoothConnectDeviceEventAction, + (state, { payload: { id, connectionStatus } }) => { + const device = state.devices.find(it => it.id === id); + + if (device !== undefined) { + device.status = connectionStatus; + } + }, + ) + .addCase(bluetoothScanStatusAction, (state, { payload: { status } }) => { + state.scanStatus = status; + }) + .addCase(deviceActions.deviceDisconnect, (state, { payload: { bluetoothProps } }) => { + if (bluetoothProps) { + state.devices = state.devices.filter(it => it.id !== bluetoothProps.id); + } + }) + .addCase( + deviceActions.connectDevice, + ( + state, + { + payload: { + device: { bluetoothProps }, + }, + }, + ) => { + if (bluetoothProps && bluetoothProps.id in state.devices) { + const deviceState = state.devices.find(it => it.id === bluetoothProps.id); + + if (deviceState !== undefined) { + deviceState.status = null; + + // Once device is fully connected, we save it to the list of paired devices + // so next time user opens suite + const foundPairedDevice = state.knownDevices.find( + it => it.id === bluetoothProps.id, + ); + if (foundPairedDevice === undefined) { + state.knownDevices.push(deviceState); + } + } + } + }, + ) + .addMatcher( + action => action.type === extra.actionTypes.storageLoad, + (state, action: AnyAction) => { + state.knownDevices = action.payload.knownDevices?.bluetooth ?? []; + }, + ), + ); +}; diff --git a/suite-common/bluetooth/tsconfig.json b/suite-common/bluetooth/tsconfig.json new file mode 100644 index 000000000000..73c744e02f0c --- /dev/null +++ b/suite-common/bluetooth/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { "outDir": "libDev" }, + "references": [ + { "path": "../redux-utils" }, + { "path": "../suite-types" }, + { "path": "../wallet-core" } + ] +} diff --git a/yarn.lock b/yarn.lock index 42b6f77ea769..9e4eea38a41b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10265,6 +10265,16 @@ __metadata: languageName: unknown linkType: soft +"@suite-common/bluetooth@workspace:suite-common/bluetooth": + version: 0.0.0-use.local + resolution: "@suite-common/bluetooth@workspace:suite-common/bluetooth" + dependencies: + "@reduxjs/toolkit": "npm:1.9.5" + "@suite-common/redux-utils": "workspace:*" + "@suite-common/wallet-core": "workspace:*" + languageName: unknown + linkType: soft + "@suite-common/connect-init@workspace:*, @suite-common/connect-init@workspace:suite-common/connect-init": version: 0.0.0-use.local resolution: "@suite-common/connect-init@workspace:suite-common/connect-init"