diff --git a/packages/suite/src/actions/bluetooth/bluetoothConnectDeviceThunk.ts b/packages/suite/src/actions/bluetooth/bluetoothConnectDeviceThunk.ts index 79457ffae86..c4193375642 100644 --- a/packages/suite/src/actions/bluetooth/bluetoothConnectDeviceThunk.ts +++ b/packages/suite/src/actions/bluetooth/bluetoothConnectDeviceThunk.ts @@ -1,4 +1,4 @@ -import { BLUETOOTH_PREFIX, bluetoothActions } from '@suite-common/bluetooth'; +import { BLUETOOTH_PREFIX } from '@suite-common/bluetooth'; import { createThunk } from '@suite-common/redux-utils'; import { notificationsActions } from '@suite-common/toast-notifications'; import { bluetoothIpc } from '@trezor/transport-bluetooth'; @@ -17,25 +17,12 @@ export const bluetoothConnectDeviceThunk = createThunk< const result = await bluetoothIpc.connectDevice(id); if (!result.success) { - dispatch( - bluetoothActions.connectDeviceEventAction({ - id, - connectionStatus: { type: 'error', error: result.error }, - }), - ); dispatch( notificationsActions.addToast({ type: 'error', error: result.error, }), ); - } else { - dispatch( - bluetoothActions.connectDeviceEventAction({ - id, - connectionStatus: { type: 'connected' }, - }), - ); } return fulfillWithValue({ success: result.success }); diff --git a/packages/suite/src/actions/bluetooth/bluetoothDisconnectDeviceThunk.ts b/packages/suite/src/actions/bluetooth/bluetoothDisconnectDeviceThunk.ts new file mode 100644 index 00000000000..37773fc7182 --- /dev/null +++ b/packages/suite/src/actions/bluetooth/bluetoothDisconnectDeviceThunk.ts @@ -0,0 +1,30 @@ +import { BLUETOOTH_PREFIX } from '@suite-common/bluetooth'; +import { createThunk } from '@suite-common/redux-utils'; +import { notificationsActions } from '@suite-common/toast-notifications'; +import { bluetoothIpc } from '@trezor/transport-bluetooth'; + +type BluetoothDisconnectDeviceThunkResult = { + success: boolean; +}; + +export const bluetoothDisconnectDeviceThunk = createThunk< + BluetoothDisconnectDeviceThunkResult, + { id: string }, + void +>( + `${BLUETOOTH_PREFIX}/bluetoothConnectDeviceThunk`, + async ({ id }, { fulfillWithValue, dispatch }) => { + const result = await bluetoothIpc.connectDevice(id); + + if (!result.success) { + dispatch( + notificationsActions.addToast({ + type: 'error', + error: result.error, + }), + ); + } + + return fulfillWithValue({ success: result.success }); + }, +); diff --git a/packages/suite/src/actions/bluetooth/initBluetoothThunk.ts b/packages/suite/src/actions/bluetooth/initBluetoothThunk.ts index f9e16b2bed3..b7863aed964 100644 --- a/packages/suite/src/actions/bluetooth/initBluetoothThunk.ts +++ b/packages/suite/src/actions/bluetooth/initBluetoothThunk.ts @@ -1,15 +1,10 @@ import { BLUETOOTH_PREFIX, bluetoothActions, selectKnownDevices } from '@suite-common/bluetooth'; import { createThunk } from '@suite-common/redux-utils/'; -import { BluetoothDevice, DeviceConnectionStatus, bluetoothIpc } from '@trezor/transport-bluetooth'; -import { Without } from '@trezor/type-utils'; +import { BluetoothDevice, bluetoothIpc } from '@trezor/transport-bluetooth'; import { remapKnownDevicesForLinux } from './remapKnownDevicesForLinux'; import { selectSuiteFlags } from '../../reducers/suite/suiteReducer'; -type DeviceConnectionStatusWithOptionalId = Without & { - id?: string; -}; - export const initBluetoothThunk = createThunk( `${BLUETOOTH_PREFIX}/initBluetoothThunk`, async (_, { dispatch, getState }) => { @@ -40,19 +35,10 @@ export const initBluetoothThunk = createThunk( dispatch(bluetoothActions.nearbyDevicesUpdateAction({ nearbyDevices })); }); - bluetoothIpc.on('device-connection-status', connectionStatus => { - console.warn('device-connection-status', connectionStatus); - const copyConnectionStatus: DeviceConnectionStatusWithOptionalId = { - ...connectionStatus, - }; - delete copyConnectionStatus.id; // So we dont pollute redux store + bluetoothIpc.on('device-update', (device: BluetoothDevice) => { + console.warn('device-update', device); - dispatch( - bluetoothActions.connectDeviceEventAction({ - id: connectionStatus.id, - connectionStatus: copyConnectionStatus, - }), - ); + dispatch(bluetoothActions.connectDeviceEventAction({ device })); }); // TODO: this should be called after trezor/connect init? diff --git a/packages/suite/src/actions/suite/storageActions.ts b/packages/suite/src/actions/suite/storageActions.ts index 85d641a29fe..c57b1d3c308 100644 --- a/packages/suite/src/actions/suite/storageActions.ts +++ b/packages/suite/src/actions/suite/storageActions.ts @@ -105,6 +105,7 @@ export const saveCoinjoinDebugSettings = () => async (_dispatch: Dispatch, getSt export const saveKnownDevices = () => async (_dispatch: Dispatch, getState: GetState) => { if (!(await db.isAccessible())) return; const { knownDevices } = getState().bluetooth; + // Todo: consider adding serializeBluetoothDevice (do not save status, ... signal strength, ...) db.addItem('knownDevices', { bluetooth: knownDevices }, 'devices', true); }; diff --git a/packages/suite/src/components/suite/bluetooth/BluetoothConnect.tsx b/packages/suite/src/components/suite/bluetooth/BluetoothConnect.tsx index ff08adfd987..1d7d87c46cf 100644 --- a/packages/suite/src/components/suite/bluetooth/BluetoothConnect.tsx +++ b/packages/suite/src/components/suite/bluetooth/BluetoothConnect.tsx @@ -7,7 +7,6 @@ import { selectKnownDevices, selectScanStatus, } from '@suite-common/bluetooth'; -import { selectDevices } from '@suite-common/wallet-core'; import { Card, Column, ElevationUp } from '@trezor/components'; import { spacings } from '@trezor/theme'; import { BluetoothDevice } from '@trezor/transport-bluetooth'; @@ -43,8 +42,6 @@ export const BluetoothConnect = ({ onClose, uiMode }: BluetoothConnectProps) => const [selectedDeviceId, setSelectedDeviceId] = useState(null); const [scannerTimerId, setScannerTimerId] = useState(null); - const trezorDevices = useSelector(selectDevices); - const bluetoothAdapterStatus = useSelector(selectAdapterStatus); const scanStatus = useSelector(selectScanStatus); const allDevices = useSelector(selectAllDevices); @@ -56,19 +53,11 @@ export const BluetoothConnect = ({ onClose, uiMode }: BluetoothConnectProps) => console.log('allDevices', allDevices); const devices = allDevices.filter(it => { - const isDeviceAlreadyConnected = - trezorDevices.find(trezorDevice => trezorDevice.bluetoothProps?.id === it.device.id) !== - undefined; - - if (isDeviceAlreadyConnected) { - return false; - } - const isDeviceUnresponsiveForTooLong = - it.device.lastUpdatedTimestamp < lasUpdatedBoundaryTimestamp; + it.lastUpdatedTimestamp < lasUpdatedBoundaryTimestamp; if (isDeviceUnresponsiveForTooLong) { - return knownDevices.find(knownDevice => knownDevice.id === it.device.id) !== undefined; + return knownDevices.find(knownDevice => knownDevice.id === it.id) !== undefined; } return true; @@ -76,7 +65,7 @@ export const BluetoothConnect = ({ onClose, uiMode }: BluetoothConnectProps) => const selectedDevice = selectedDeviceId !== null - ? devices.find(device => device.device.id === selectedDeviceId) + ? devices.find(device => device.id === selectedDeviceId) : undefined; useEffect(() => { @@ -113,7 +102,7 @@ export const BluetoothConnect = ({ onClose, uiMode }: BluetoothConnectProps) => setScannerTimerId(timerId); }; - const onSelect = async (id: string) => { + const onConnect = async (id: string) => { setSelectedDeviceId(id); const result = await dispatch(bluetoothConnectDeviceThunk({ id })).unwrap(); @@ -145,14 +134,14 @@ export const BluetoothConnect = ({ onClose, uiMode }: BluetoothConnectProps) => if ( selectedDevice !== undefined && - selectedDevice.status !== null && - selectedDevice.status.type === 'pairing' && - (selectedDevice.status.pin?.length ?? 0) > 0 + selectedDevice !== null && + selectedDevice.connectionStatus.type === 'pairing' && + (selectedDevice.connectionStatus?.pin?.length ?? 0) > 0 ) { return ( ); @@ -173,7 +162,7 @@ export const BluetoothConnect = ({ onClose, uiMode }: BluetoothConnectProps) => ) : ( diff --git a/packages/suite/src/components/suite/bluetooth/BluetoothDeviceItem.tsx b/packages/suite/src/components/suite/bluetooth/BluetoothDeviceItem.tsx index c2d7182f6b0..13ad825ad16 100644 --- a/packages/suite/src/components/suite/bluetooth/BluetoothDeviceItem.tsx +++ b/packages/suite/src/components/suite/bluetooth/BluetoothDeviceItem.tsx @@ -1,38 +1,61 @@ -import { BluetoothDeviceState, DeviceBluetoothStatusType } from '@suite-common/bluetooth'; +import { DeviceBluetoothConnectionStatusType } from '@suite-common/bluetooth'; import { Button, Row } from '@trezor/components'; import { spacings } from '@trezor/theme'; import { BluetoothDevice } from '@trezor/transport-bluetooth'; import { BluetoothDeviceComponent } from './BluetoothDeviceComponent'; +import { bluetoothDisconnectDeviceThunk } from '../../../actions/bluetooth/bluetoothDisconnectDeviceThunk'; +import { useDispatch } from '../../../hooks/suite'; -const map: Record = { +const labelMap: Record = { + disconnected: 'Connect', connecting: 'Connecting', - connected: 'Connected', - error: 'Error', + connected: 'Disconnect', + 'connection-error': 'Try again', // Out-of-range, offline, in the faraday cage, ... pairing: 'Pairing', paired: 'Paired', + 'pairing-error': '', // shall never be show to user }; +const LOADING_STATUSES: DeviceBluetoothConnectionStatusType[] = ['pairing', 'connecting']; + type BluetoothDeviceItemProps = { - device: BluetoothDeviceState; - onClick: () => void; + device: BluetoothDevice; + onConnect: () => void; }; -export const BluetoothDeviceItem = ({ device, onClick }: BluetoothDeviceItemProps) => { - const isDisabled = device.status !== null; - const isLoading = device.status?.type === 'pairing' || device.status?.type === 'connecting'; +export const BluetoothDeviceItem = ({ device, onConnect }: BluetoothDeviceItemProps) => { + const dispatch = useDispatch(); + + const isDisabled = device.connectionStatus.type !== 'disconnected'; + const isLoading = LOADING_STATUSES.includes(device.connectionStatus.type); + + const onDisconnect = () => { + dispatch(bluetoothDisconnectDeviceThunk({ id: device.id })).unwrap(); + }; + + const onClickMap: Record void) | null> = { + 'connection-error': onConnect, + 'pairing-error': null, + connected: onDisconnect, + connecting: null, + disconnected: onConnect, + paired: null, + pairing: null, + }; return ( - - + + ); diff --git a/packages/suite/src/components/suite/bluetooth/BluetoothDeviceList.tsx b/packages/suite/src/components/suite/bluetooth/BluetoothDeviceList.tsx index f7bf88dac89..e3265eea165 100644 --- a/packages/suite/src/components/suite/bluetooth/BluetoothDeviceList.tsx +++ b/packages/suite/src/components/suite/bluetooth/BluetoothDeviceList.tsx @@ -1,4 +1,3 @@ -import { BluetoothDeviceState } from '@suite-common/bluetooth'; import { Card, Column, Row, SkeletonRectangle } from '@trezor/components'; import { spacings } from '@trezor/theme'; import { BluetoothDevice } from '@trezor/transport-bluetooth'; @@ -6,8 +5,8 @@ import { BluetoothDevice } from '@trezor/transport-bluetooth'; import { BluetoothDeviceItem } from './BluetoothDeviceItem'; type BluetoothDeviceListProps = { - deviceList: BluetoothDeviceState[]; - onSelect: (id: string) => void; + deviceList: BluetoothDevice[]; + onConnect: (id: string) => void; isScanning: boolean; isDisabled: boolean; }; @@ -24,19 +23,19 @@ const SkeletonDevice = () => ( ); export const BluetoothDeviceList = ({ - onSelect, + onConnect, deviceList, isScanning, }: BluetoothDeviceListProps) => ( {deviceList.map(device => ( - onSelect(device.device.id)} - /> - ))} + onConnect(device.id)} + /> + ))} {isScanning && } diff --git a/packages/suite/src/components/suite/bluetooth/BluetoothSelectedDevice.tsx b/packages/suite/src/components/suite/bluetooth/BluetoothSelectedDevice.tsx index ddb6750baf1..bf7726b20a4 100644 --- a/packages/suite/src/components/suite/bluetooth/BluetoothSelectedDevice.tsx +++ b/packages/suite/src/components/suite/bluetooth/BluetoothSelectedDevice.tsx @@ -1,6 +1,6 @@ import { ReactNode } from 'react'; -import { BluetoothDeviceState, DeviceBluetoothStatusType } from '@suite-common/bluetooth'; +import { BluetoothDeviceState, DeviceBluetoothConnectionStatusType } from '@suite-common/bluetooth'; import { Button, Card, @@ -44,7 +44,7 @@ export type OkComponentProps = { }; const OkComponent = ({ device, onCancel }: OkComponentProps) => { - const map: Record = { + const map: Record = { connecting: , connected: null, error: null, diff --git a/packages/suite/src/middlewares/wallet/storageMiddleware.ts b/packages/suite/src/middlewares/wallet/storageMiddleware.ts index a0402308d5f..ca9097f0d78 100644 --- a/packages/suite/src/middlewares/wallet/storageMiddleware.ts +++ b/packages/suite/src/middlewares/wallet/storageMiddleware.ts @@ -204,9 +204,10 @@ const storageMiddleware = (api: MiddlewareAPI) => { } if ( - deviceActions.connectDevice.match(action) || + deviceActions.connectDevice.match(action) || // Known device is stored bluetoothActions.knownDevicesUpdateAction.match(action) || - bluetoothActions.removeKnownDeviceAction.match(action) + bluetoothActions.removeKnownDeviceAction.match(action) || + bluetoothActions.connectDeviceEventAction.match(action) // Known devices may be updated ) { api.dispatch(storageActions.saveKnownDevices()); } diff --git a/suite-common/bluetooth/src/bluetoothActions.ts b/suite-common/bluetooth/src/bluetoothActions.ts index bf43e7a7ec2..0fe56e0ea46 100644 --- a/suite-common/bluetooth/src/bluetoothActions.ts +++ b/suite-common/bluetooth/src/bluetoothActions.ts @@ -1,10 +1,6 @@ import { createAction } from '@reduxjs/toolkit'; -import { - BluetoothDeviceCommon, - BluetoothScanStatus, - DeviceBluetoothStatus, -} from './bluetoothReducer'; +import { BluetoothDeviceCommon, BluetoothScanStatus } from './bluetoothReducer'; export const BLUETOOTH_PREFIX = '@suite/bluetooth'; @@ -44,8 +40,8 @@ const removeKnownDeviceAction = createAction( const connectDeviceEventAction = createAction( `${BLUETOOTH_PREFIX}/connect-device-event`, - ({ connectionStatus, id }: { id: string; connectionStatus: DeviceBluetoothStatus }) => ({ - payload: { id, connectionStatus }, + ({ device }: { device: BluetoothDeviceCommon }) => ({ + payload: { device }, }), ); diff --git a/suite-common/bluetooth/src/bluetoothReducer.ts b/suite-common/bluetooth/src/bluetoothReducer.ts index b092ad39093..aa0ce9f51ef 100644 --- a/suite-common/bluetooth/src/bluetoothReducer.ts +++ b/suite-common/bluetooth/src/bluetoothReducer.ts @@ -7,14 +7,19 @@ import { bluetoothActions } from './bluetoothActions'; export type BluetoothScanStatus = 'idle' | 'running' | 'error'; -export type DeviceBluetoothStatus = +export type DeviceBluetoothConnectionStatus = + | { type: 'disconnected' } | { type: 'pairing'; pin?: string } | { type: 'paired' } | { type: 'connecting' } | { type: 'connected' } | { - type: 'error'; + type: 'pairing-error'; // This device cannot be paired ever again (new macAddress, new device) error: string; + } + | { + type: 'connection-error'; // Out-of-range, offline, in the faraday cage, ... + error: string; // Timeout, connection aborted, ... }; // Do not export this outside of this suite-common package, Suite uses ist own type @@ -24,19 +29,15 @@ export type BluetoothDeviceCommon = { name: string; data: number[]; // Todo: consider typed data-structure for this lastUpdatedTimestamp: number; + connectionStatus: DeviceBluetoothConnectionStatus; }; -export type DeviceBluetoothStatusType = DeviceBluetoothStatus['type']; - -export type BluetoothDeviceState = { - device: T; - status: DeviceBluetoothStatus | null; -}; +export type DeviceBluetoothConnectionStatusType = DeviceBluetoothConnectionStatus['type']; export type BluetoothState = { adapterStatus: 'unknown' | 'enabled' | 'disabled'; scanStatus: BluetoothScanStatus; - nearbyDevices: BluetoothDeviceState[]; + nearbyDevices: T[]; // Must be sorted, newest last // 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 @@ -47,7 +48,7 @@ export const prepareBluetoothReducerCreator = ( const initialState: BluetoothState = { adapterStatus: 'unknown', scanStatus: 'idle', - nearbyDevices: [] as BluetoothDeviceState[], + nearbyDevices: [] as T[], knownDevices: [] as T[], }; @@ -63,26 +64,21 @@ export const prepareBluetoothReducerCreator = ( .addCase( bluetoothActions.nearbyDevicesUpdateAction, (state, { payload: { nearbyDevices } }) => { - state.nearbyDevices = nearbyDevices - .sort((a, b) => b.lastUpdatedTimestamp - a.lastUpdatedTimestamp) - .map( - (device): Draft> => ({ - device: device as Draft, - status: - state.nearbyDevices.find(it => it.device.id === device.id) - ?.status ?? null, - }), - ); + state.nearbyDevices = nearbyDevices.sort( + (a, b) => b.lastUpdatedTimestamp - a.lastUpdatedTimestamp, + ) as Draft[]; }, ) .addCase( bluetoothActions.connectDeviceEventAction, - (state, { payload: { id, connectionStatus } }) => { - const device = state.nearbyDevices.find(it => it.device.id === id); + (state, { payload: { device } }) => { + state.nearbyDevices = state.nearbyDevices.map(it => + it.id === device.id ? device : it, + ) as Draft[]; - if (device !== undefined) { - device.status = connectionStatus; - } + state.knownDevices = state.knownDevices.map(it => + it.id === device.id ? device : it, + ) as Draft[]; }, ) .addCase( @@ -102,7 +98,7 @@ export const prepareBluetoothReducerCreator = ( .addCase(deviceActions.deviceDisconnect, (state, { payload: { bluetoothProps } }) => { if (bluetoothProps !== undefined) { state.nearbyDevices = state.nearbyDevices.filter( - it => it.device.id !== bluetoothProps.id, + it => it.id !== bluetoothProps.id, ); } }) @@ -120,18 +116,16 @@ export const prepareBluetoothReducerCreator = ( return; } - const deviceState = state.nearbyDevices.find( - it => it.device.id === bluetoothProps.id, - ); + const device = state.nearbyDevices.find(it => it.id === bluetoothProps.id); - if (deviceState !== undefined) { + if (device !== undefined) { // Once device is fully connected, we save it to the list of known devices // so next time user opens suite we can automatically connect to it. const foundKnownDevice = state.knownDevices.find( it => it.id === bluetoothProps.id, ); if (foundKnownDevice === undefined) { - state.knownDevices.push(deviceState.device); + state.knownDevices.push(device); } } }, @@ -139,7 +133,15 @@ export const prepareBluetoothReducerCreator = ( .addMatcher( action => action.type === extra.actionTypes.storageLoad, (state, action: AnyAction) => { - state.knownDevices = action.payload.knownDevices?.bluetooth ?? []; + const loadedKnownDevices = (action.payload.knownDevices?.bluetooth ?? + []) as T[]; + + state.knownDevices = loadedKnownDevices.map( + (it): T => ({ + ...it, + status: { type: 'disconnected' }, + }), + ) as Draft[]; }, ), ); diff --git a/suite-common/bluetooth/src/bluetoothSelectors.ts b/suite-common/bluetooth/src/bluetoothSelectors.ts index 7dce9bb2382..e2b66f06846 100644 --- a/suite-common/bluetooth/src/bluetoothSelectors.ts +++ b/suite-common/bluetooth/src/bluetoothSelectors.ts @@ -1,6 +1,6 @@ import { createWeakMapSelector } from '@suite-common/redux-utils'; -import { BluetoothDeviceCommon, BluetoothDeviceState, BluetoothState } from './bluetoothReducer'; +import { BluetoothDeviceCommon, BluetoothState } from './bluetoothReducer'; export type WithBluetoothState = { bluetooth: BluetoothState; @@ -25,26 +25,22 @@ export const prepareSelectAllDevices = () => createWeakMapSelector.withTypes>()( [state => state.bluetooth.nearbyDevices, state => state.bluetooth.knownDevices], (nearbyDevices, knownDevices) => { - const map = new Map>(); + const map = new Map(); + + knownDevices.forEach(knownDevice => map.set(knownDevice.id, knownDevice)); nearbyDevices - // Devices with error status should be displayed in the list, as it - // won't be possible to connect to them ever again. User has to start - // pairing again, which would produce a device with new id. - .filter(nearbyDevice => nearbyDevice.status?.type !== 'error') + .reverse() + // Devices with 'pairing-error' status should NOT be displayed in the list, as it + // won't be possible to connect to them ever again. User has to start pairing again, + // which would produce a device with new id. + .filter(nearbyDevice => nearbyDevice.connectionStatus?.type !== 'pairing-error') .forEach(nearbyDevice => { - map.set(nearbyDevice.device.id, nearbyDevice); + map.delete(nearbyDevice.id); // Delete and re-add to change the order, replace would keep original order + map.set(nearbyDevice.id, nearbyDevice); }); - knownDevices.forEach(knownDevice => { - // All known devices are automatically considered being in 'connecting' state - // underlying code should connect them automatically. - map.set(knownDevice.id, { device: knownDevice, status: { type: 'connecting' } }); - }); - - return Array.from(map.values()).sort( - (a, b) => b.device.lastUpdatedTimestamp - a.device.lastUpdatedTimestamp, - ); + return Array.from(map.values()); }, ); diff --git a/suite-common/bluetooth/src/index.ts b/suite-common/bluetooth/src/index.ts index 7fe15e21c01..aba66fc3796 100644 --- a/suite-common/bluetooth/src/index.ts +++ b/suite-common/bluetooth/src/index.ts @@ -2,9 +2,8 @@ export { BLUETOOTH_PREFIX, bluetoothActions } from './bluetoothActions'; export { prepareBluetoothReducerCreator } from './bluetoothReducer'; export type { - BluetoothDeviceState, BluetoothScanStatus, - DeviceBluetoothStatusType, + DeviceBluetoothConnectionStatusType, } from './bluetoothReducer'; export { diff --git a/suite-common/bluetooth/tests/bluetoothReducer.test.ts b/suite-common/bluetooth/tests/bluetoothReducer.test.ts index b6a0af53c30..551f0c0b503 100644 --- a/suite-common/bluetooth/tests/bluetoothReducer.test.ts +++ b/suite-common/bluetooth/tests/bluetoothReducer.test.ts @@ -5,7 +5,7 @@ import { configureMockStore, extraDependenciesMock } from '@suite-common/test-ut import { deviceActions } from '@suite-common/wallet-core'; import { Device } from '@trezor/connect'; -import { BluetoothDeviceState, bluetoothActions, prepareBluetoothReducerCreator } from '../src'; +import { bluetoothActions, prepareBluetoothReducerCreator } from '../src'; import { BluetoothDeviceCommon, BluetoothState } from '../src/bluetoothReducer'; const bluetoothReducer = @@ -14,38 +14,24 @@ const bluetoothReducer = const initialState: BluetoothState = { adapterStatus: 'unknown', scanStatus: 'idle', - nearbyDevices: [] as BluetoothDeviceState[], + nearbyDevices: [] as BluetoothDeviceCommon[], knownDevices: [] as BluetoothDeviceCommon[], }; -const bluetoothStateDeviceA: BluetoothDeviceState = { - device: { - id: 'A', - data: [], - name: 'Trezor A', - lastUpdatedTimestamp: 1, - }, - status: { type: 'pairing' }, +const pairingDeviceA: BluetoothDeviceCommon = { + id: 'A', + data: [], + name: 'Trezor A', + lastUpdatedTimestamp: 1, + connectionStatus: { type: 'pairing' }, }; -const bluetoothStateDeviceB: BluetoothDeviceState = { - device: { - id: 'B', - data: [], - name: 'Trezor B', - lastUpdatedTimestamp: 2, - }, - status: null, -}; - -const bluetoothStateDeviceC: BluetoothDeviceState = { - device: { - id: 'C', - data: [], - name: 'Trezor C', - lastUpdatedTimestamp: 3, - }, - status: null, +const disconnectedDeviceB: BluetoothDeviceCommon = { + id: 'B', + data: [], + name: 'Trezor B', + lastUpdatedTimestamp: 2, + connectionStatus: { type: 'disconnected' }, }; describe('bluetoothReducer', () => { @@ -63,53 +49,27 @@ describe('bluetoothReducer', () => { expect(store.getState().bluetooth.adapterStatus).toEqual('disabled'); }); - it('sorts the devices based on the `lastUpdatedTimestamp` and keeps the status for already existing device', () => { - const store = configureMockStore({ - extra: {}, - reducer: combineReducers({ bluetooth: bluetoothReducer }), - preloadedState: { - bluetooth: { - ...initialState, - nearbyDevices: [bluetoothStateDeviceB, bluetoothStateDeviceA], - }, - }, - }); - - const nearbyDevices: BluetoothDeviceCommon[] = [ - bluetoothStateDeviceA.device, - bluetoothStateDeviceC.device, - ]; - - store.dispatch(bluetoothActions.nearbyDevicesUpdateAction({ nearbyDevices })); - expect(store.getState().bluetooth.nearbyDevices).toEqual([ - bluetoothStateDeviceC, - // No `B` device present, it was dropped - { - device: bluetoothStateDeviceA.device, - status: { type: 'pairing' }, // Keeps the pairing status - }, - ]); - }); - it('changes the status of the given device during pairing process', () => { const store = configureMockStore({ extra: {}, reducer: combineReducers({ bluetooth: bluetoothReducer }), preloadedState: { - bluetooth: { ...initialState, nearbyDevices: [bluetoothStateDeviceA] }, + bluetooth: { ...initialState, nearbyDevices: [pairingDeviceA] }, }, }); store.dispatch( bluetoothActions.connectDeviceEventAction({ - id: 'A', - connectionStatus: { type: 'pairing', pin: '12345' }, + device: { + ...pairingDeviceA, + connectionStatus: { type: 'pairing', pin: '12345' }, + }, }), ); expect(store.getState().bluetooth.nearbyDevices).toEqual([ { - device: bluetoothStateDeviceA.device, - status: { type: 'pairing', pin: '12345' }, + ...pairingDeviceA, + connectionStatus: { type: 'pairing', pin: '12345' }, }, ]); }); @@ -121,10 +81,7 @@ describe('bluetoothReducer', () => { preloadedState: { bluetooth: initialState }, }); - const knownDeviceToAdd: BluetoothDeviceCommon[] = [ - bluetoothStateDeviceA.device, - bluetoothStateDeviceB.device, - ]; + const knownDeviceToAdd: BluetoothDeviceCommon[] = [pairingDeviceA, disconnectedDeviceB]; store.dispatch( bluetoothActions.knownDevicesUpdateAction({ knownDevices: knownDeviceToAdd }), @@ -133,7 +90,7 @@ describe('bluetoothReducer', () => { store.dispatch(bluetoothActions.removeKnownDeviceAction({ id: 'A' })); - expect(store.getState().bluetooth.knownDevices).toEqual([bluetoothStateDeviceB.device]); + expect(store.getState().bluetooth.knownDevices).toEqual([disconnectedDeviceB]); }); it('removes device from nearbyDevices when the device is disconnected by TrezorConnect', () => { @@ -141,7 +98,7 @@ describe('bluetoothReducer', () => { extra: {}, reducer: combineReducers({ bluetooth: bluetoothReducer }), preloadedState: { - bluetooth: { ...initialState, nearbyDevices: [bluetoothStateDeviceA] }, + bluetooth: { ...initialState, nearbyDevices: [pairingDeviceA] }, }, }); @@ -154,9 +111,9 @@ describe('bluetoothReducer', () => { }); it('stores a device in `knownDevices` when device is connected by TrezorConnect', () => { - const nearbyDevice: BluetoothDeviceState = { - device: bluetoothStateDeviceA.device, - status: { type: 'connected' }, + const nearbyDevice: BluetoothDeviceCommon = { + ...pairingDeviceA, + connectionStatus: { type: 'connected' }, }; const store = configureMockStore({ @@ -177,6 +134,6 @@ describe('bluetoothReducer', () => { settings: { defaultWalletLoading: 'passphrase' }, }), ); - expect(store.getState().bluetooth.knownDevices).toEqual([nearbyDevice.device]); + expect(store.getState().bluetooth.knownDevices).toEqual([nearbyDevice]); }); }); diff --git a/suite-common/bluetooth/tests/bluetoothSelectors.test.ts b/suite-common/bluetooth/tests/bluetoothSelectors.test.ts index 5b92143e353..455ba8cc43e 100644 --- a/suite-common/bluetooth/tests/bluetoothSelectors.test.ts +++ b/suite-common/bluetooth/tests/bluetoothSelectors.test.ts @@ -1,39 +1,36 @@ -import { BluetoothDeviceState, prepareSelectAllDevices } from '../src'; +import { prepareSelectAllDevices } from '../src'; import { BluetoothDeviceCommon, BluetoothState } from '../src/bluetoothReducer'; import { WithBluetoothState } from '../src/bluetoothSelectors'; const initialState: BluetoothState = { adapterStatus: 'unknown', scanStatus: 'idle', - nearbyDevices: [] as BluetoothDeviceState[], + nearbyDevices: [] as BluetoothDeviceCommon[], knownDevices: [] as BluetoothDeviceCommon[], }; -const pairingDeviceStateA: BluetoothDeviceState = { - device: { - id: 'A', - data: [], - name: 'Trezor A', - lastUpdatedTimestamp: 1, - }, - status: { type: 'pairing' }, +const pairingDeviceStateA: BluetoothDeviceCommon = { + id: 'A', + data: [], + name: 'Trezor A', + lastUpdatedTimestamp: 1, + connectionStatus: { type: 'pairing' }, }; -const deviceB: BluetoothDeviceCommon = { +const disconenctedDeviceB: BluetoothDeviceCommon = { id: 'B', data: [], name: 'Trezor B', lastUpdatedTimestamp: 2, + connectionStatus: { type: 'disconnected' }, }; -const pairingDeviceStateC: BluetoothDeviceState = { - device: { - id: 'C', - data: [], - name: 'Trezor C', - lastUpdatedTimestamp: 3, - }, - status: { type: 'pairing' }, +const pairingDeviceStateC: BluetoothDeviceCommon = { + id: 'C', + data: [], + name: 'Trezor C', + lastUpdatedTimestamp: 3, + connectionStatus: { type: 'pairing' }, }; describe('bluetoothSelectors', () => { @@ -43,20 +40,47 @@ describe('bluetoothSelectors', () => { const state: WithBluetoothState = { bluetooth: { ...initialState, - nearbyDevices: [pairingDeviceStateA, pairingDeviceStateC], - knownDevices: [pairingDeviceStateA.device, deviceB], + knownDevices: [pairingDeviceStateA, disconenctedDeviceB], + nearbyDevices: [ + { + ...pairingDeviceStateA, + connectionStatus: { type: 'connected' }, + }, + pairingDeviceStateC, + ], }, }; const devices = selectAllDevices(state); expect(devices).toEqual([ - pairingDeviceStateC, - { device: deviceB, status: { type: 'connecting' } }, - { device: pairingDeviceStateA.device, status: { type: 'connecting' } }, // overrides to 'connecting' + { ...disconenctedDeviceB, connectionStatus: { type: 'disconnected' } }, // from knownDevices only, first in the list + pairingDeviceStateC, // from nearbyDevices only + { ...pairingDeviceStateA, connectionStatus: { type: 'connected' } }, // override by nearbyDevices ]); const devicesSecondTime = selectAllDevices(state); expect(devices === devicesSecondTime).toBe(true); // Asserts that `reselect` memoization works }); + + it('filters out the device with pairing-error', () => { + const selectAllDevices = prepareSelectAllDevices(); + + const state: WithBluetoothState = { + bluetooth: { + ...initialState, + knownDevices: [], + nearbyDevices: [ + { + ...pairingDeviceStateA, + connectionStatus: { type: 'pairing-error', error: '' }, + }, + ], + }, + }; + + const devices = selectAllDevices(state); + + expect(devices).toEqual([]); + }); });