From 4ee3a380638ddebdfa95ac6fb3e60b1e82d4c06f Mon Sep 17 00:00:00 2001 From: Peter Sanderson Date: Thu, 27 Feb 2025 11:41:23 +0100 Subject: [PATCH 1/5] feat: update shared state fro bluetooth to accomodate changes in the ipc --- .../bluetooth/src/bluetoothActions.ts | 10 +- .../bluetooth/src/bluetoothReducer.ts | 66 +++++++------ .../bluetooth/src/bluetoothSelectors.ts | 29 +++--- suite-common/bluetooth/src/index.ts | 3 +- .../bluetooth/tests/bluetoothReducer.test.ts | 99 ++++++------------- .../tests/bluetoothSelectors.test.ts | 68 ++++++++++--- 6 files changed, 135 insertions(+), 140 deletions(-) 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 64cd49cad38..4526efef1cf 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,21 +25,24 @@ export const prepareSelectAllDevices = () => createWeakMapSelector.withTypes>()( [state => state.bluetooth.nearbyDevices, state => state.bluetooth.knownDevices], (nearbyDevices, knownDevices) => { - const map = new Map>(); + const map = new Map(); - nearbyDevices.forEach(nearbyDevice => { - map.set(nearbyDevice.device.id, nearbyDevice); - }); + knownDevices.forEach(knownDevice => map.set(knownDevice.id, knownDevice)); - knownDevices.forEach(knownDevice => { - if (!map.has(knownDevice.id)) { - map.set(knownDevice.id, { device: knownDevice, status: null }); - } - }); + const nearbyDevicesCopy = [...nearbyDevices]; + nearbyDevicesCopy.reverse(); - return Array.from(map.values()).sort( - (a, b) => b.device.lastUpdatedTimestamp - a.device.lastUpdatedTimestamp, - ); + nearbyDevicesCopy + // 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.delete(nearbyDevice.id); // Delete and re-add to change the order, replace would keep original order + map.set(nearbyDevice.id, nearbyDevice); + }); + + 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 375dddd75c4..455ba8cc43e 100644 --- a/suite-common/bluetooth/tests/bluetoothSelectors.test.ts +++ b/suite-common/bluetooth/tests/bluetoothSelectors.test.ts @@ -1,48 +1,86 @@ -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: BluetoothDeviceCommon = { + id: 'C', + data: [], + name: 'Trezor C', + lastUpdatedTimestamp: 3, + connectionStatus: { type: 'pairing' }, }; describe('bluetoothSelectors', () => { - it('selects knownDevices and nearbyDevices in one list fot the UI', () => { + it('selects knownDevices and nearbyDevices in one list fot the UI, all known devices are', () => { const selectAllDevices = prepareSelectAllDevices(); const state: WithBluetoothState = { bluetooth: { ...initialState, - nearbyDevices: [pairingDeviceStateA], - knownDevices: [pairingDeviceStateA.device, deviceB], + knownDevices: [pairingDeviceStateA, disconenctedDeviceB], + nearbyDevices: [ + { + ...pairingDeviceStateA, + connectionStatus: { type: 'connected' }, + }, + pairingDeviceStateC, + ], }, }; const devices = selectAllDevices(state); - expect(devices).toEqual([{ device: deviceB, status: null }, pairingDeviceStateA]); + expect(devices).toEqual([ + { ...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([]); + }); }); From 937dae219e484c30027deecf09780be7dfb4b8e9 Mon Sep 17 00:00:00 2001 From: Peter Sanderson Date: Thu, 27 Feb 2025 11:41:53 +0100 Subject: [PATCH 2/5] fix: temporary fix for disconnect --- .../src/client/bluetooth-ipc-main.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/transport-bluetooth/src/client/bluetooth-ipc-main.ts b/packages/transport-bluetooth/src/client/bluetooth-ipc-main.ts index 684017ed214..4f68452aec1 100644 --- a/packages/transport-bluetooth/src/client/bluetooth-ipc-main.ts +++ b/packages/transport-bluetooth/src/client/bluetooth-ipc-main.ts @@ -161,12 +161,15 @@ export class BluetoothIpc extends TypedEmitter implements Bl return { success: true } as const; } - disconnectDevice(id: string) { - if (id) { - throw new Error('TODO BluetoothIpc.disconnect'); - } + async disconnectDevice(id: string) { + try { + await this.api.connect(); + await this.api.send('disconnect_device', id); - return Promise.resolve({ success: true, payload: true } as const); + return { success: true } as const; + } catch (error) { + return { success: false, error: error.message }; + } } async forgetDevice(id: string) { From 9a68d096a54a821afb817719e9276f208c9da8fc Mon Sep 17 00:00:00 2001 From: Peter Sanderson Date: Thu, 27 Feb 2025 11:42:29 +0100 Subject: [PATCH 3/5] feat: progress on the UI, implement new structure of the bluetooth redux state and implement connect/disconnect --- .../remapKnownDevicesForLinux.test.ts | 4 + .../bluetooth/bluetoothConnectDeviceThunk.ts | 17 +-- .../bluetoothDisconnectDeviceThunk.ts | 30 ++++++ .../actions/bluetooth/initBluetoothThunk.ts | 9 +- .../suite/src/actions/suite/storageActions.ts | 1 + .../suite/bluetooth/BluetoothConnect.tsx | 47 ++++---- .../bluetooth/BluetoothDeviceComponent.tsx | 4 +- .../suite/bluetooth/BluetoothDeviceItem.tsx | 97 ++++++++++++++--- .../suite/bluetooth/BluetoothDeviceList.tsx | 26 ++--- .../bluetooth/BluetoothSelectedDevice.tsx | 101 +++++++++++++----- .../suite/bluetooth/BluetoothTips.tsx | 13 ++- .../middlewares/wallet/storageMiddleware.ts | 5 +- 12 files changed, 247 insertions(+), 107 deletions(-) create mode 100644 packages/suite/src/actions/bluetooth/bluetoothDisconnectDeviceThunk.ts diff --git a/packages/suite/src/actions/bluetooth/__tests__/remapKnownDevicesForLinux.test.ts b/packages/suite/src/actions/bluetooth/__tests__/remapKnownDevicesForLinux.test.ts index e0e42d4816d..38dff71dcbe 100644 --- a/packages/suite/src/actions/bluetooth/__tests__/remapKnownDevicesForLinux.test.ts +++ b/packages/suite/src/actions/bluetooth/__tests__/remapKnownDevicesForLinux.test.ts @@ -11,6 +11,7 @@ const nearbyDeviceA: BluetoothDevice = { connected: false, paired: false, rssi: 0, + connectionStatus: { type: 'pairing' }, }; const nearbyDeviceC: BluetoothDevice = { @@ -22,6 +23,7 @@ const nearbyDeviceC: BluetoothDevice = { connected: false, paired: false, rssi: 0, + connectionStatus: { type: 'pairing' }, }; const knownDeviceB: BluetoothDevice = { @@ -33,6 +35,7 @@ const knownDeviceB: BluetoothDevice = { connected: false, paired: false, rssi: 0, + connectionStatus: { type: 'pairing' }, }; const knownDeviceA: BluetoothDevice = { @@ -44,6 +47,7 @@ const knownDeviceA: BluetoothDevice = { connected: false, paired: false, rssi: 0, + connectionStatus: { type: 'pairing' }, }; describe(remapKnownDevicesForLinux.name, () => { diff --git a/packages/suite/src/actions/bluetooth/bluetoothConnectDeviceThunk.ts b/packages/suite/src/actions/bluetooth/bluetoothConnectDeviceThunk.ts index 79457ffae86..a92e2df9222 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,14 @@ export const bluetoothConnectDeviceThunk = createThunk< const result = await bluetoothIpc.connectDevice(id); if (!result.success) { - dispatch( - bluetoothActions.connectDeviceEventAction({ - id, - connectionStatus: { type: 'error', error: result.error }, - }), - ); + console.log('_______bluetoothConnectDeviceThunk :: result', result); + 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..cf4a878a3dc --- /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.disconnectDevice(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 1e893880082..b7863aed964 100644 --- a/packages/suite/src/actions/bluetooth/initBluetoothThunk.ts +++ b/packages/suite/src/actions/bluetooth/initBluetoothThunk.ts @@ -35,15 +35,10 @@ export const initBluetoothThunk = createThunk( dispatch(bluetoothActions.nearbyDevicesUpdateAction({ nearbyDevices })); }); - bluetoothIpc.on('device-update', device => { + bluetoothIpc.on('device-update', (device: BluetoothDevice) => { console.warn('device-update', device); - dispatch( - bluetoothActions.connectDeviceEventAction({ - id: device.id, - connectionStatus: device.connectionStatus as any, // TODO: type - }), - ); + 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 c95c6f85f72..56215e5d68a 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'; @@ -21,10 +20,8 @@ import { BluetoothSelectedDevice } from './BluetoothSelectedDevice'; import { BluetoothTips } from './BluetoothTips'; import { BluetoothNotEnabled } from './errors/BluetoothNotEnabled'; import { BluetoothVersionNotCompatible } from './errors/BluetoothVersionNotCompatible'; -import { bluetoothConnectDeviceThunk } from '../../../actions/bluetooth/bluetoothConnectDeviceThunk'; import { bluetoothStartScanningThunk } from '../../../actions/bluetooth/bluetoothStartScanningThunk'; import { bluetoothStopScanningThunk } from '../../../actions/bluetooth/bluetoothStopScanningThunk'; -import { closeModalApp } from '../../../actions/suite/routerActions'; import { useDispatch, useSelector } from '../../../hooks/suite'; const SCAN_TIMEOUT = 30_000; @@ -43,8 +40,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); @@ -53,20 +48,14 @@ export const BluetoothConnect = ({ onClose, uiMode }: BluetoothConnectProps) => const lasUpdatedBoundaryTimestamp = Date.now() / 1000 - UNPAIRED_DEVICES_LAST_UPDATED_LIMIT_SECONDS; - const devices = allDevices.filter(it => { - const isDeviceAlreadyConnected = - trezorDevices.find(trezorDevice => trezorDevice.bluetoothProps?.id === it.device.id) !== - undefined; - - if (isDeviceAlreadyConnected) { - return false; - } + console.log('allDevices', allDevices); + const devices = allDevices.filter(it => { 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; @@ -74,7 +63,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(() => { @@ -111,13 +100,8 @@ export const BluetoothConnect = ({ onClose, uiMode }: BluetoothConnectProps) => setScannerTimerId(timerId); }; - const onSelect = async (id: string) => { + const onSelect = (id: string) => { setSelectedDeviceId(id); - const result = await dispatch(bluetoothConnectDeviceThunk({ id })).unwrap(); - - if (uiMode === 'card' && result.success) { - dispatch(closeModalApp()); - } }; if (bluetoothAdapterStatus === 'disabled') { @@ -143,21 +127,27 @@ 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 ( ); } if (selectedDevice !== undefined) { - return ; + return ( + + ); } const content = scanFailed ? ( @@ -168,6 +158,7 @@ export const BluetoothConnect = ({ onClose, uiMode }: BluetoothConnectProps) => onSelect={onSelect} deviceList={devices} isScanning={isScanning} + uiMode={uiMode} /> ); diff --git a/packages/suite/src/components/suite/bluetooth/BluetoothDeviceComponent.tsx b/packages/suite/src/components/suite/bluetooth/BluetoothDeviceComponent.tsx index 37fa30d4605..aaf0db000f3 100644 --- a/packages/suite/src/components/suite/bluetooth/BluetoothDeviceComponent.tsx +++ b/packages/suite/src/components/suite/bluetooth/BluetoothDeviceComponent.tsx @@ -36,7 +36,9 @@ export const BluetoothDeviceComponent = ({ device, flex, margin }: BluetoothDevi Trezor Safe 7 - + +
{device.macAddress}
+
{colorName} diff --git a/packages/suite/src/components/suite/bluetooth/BluetoothDeviceItem.tsx b/packages/suite/src/components/suite/bluetooth/BluetoothDeviceItem.tsx index 082fabdac1f..15c5d047786 100644 --- a/packages/suite/src/components/suite/bluetooth/BluetoothDeviceItem.tsx +++ b/packages/suite/src/components/suite/bluetooth/BluetoothDeviceItem.tsx @@ -1,25 +1,88 @@ +import { useState } from 'react'; + +import { DeviceBluetoothConnectionStatusType } from '@suite-common/bluetooth'; import { Button, Row } from '@trezor/components'; import { spacings } from '@trezor/theme'; -import { BluetoothDevice as BluetoothDeviceType } from '@trezor/transport-bluetooth'; +import { BluetoothDevice } from '@trezor/transport-bluetooth'; import { BluetoothDeviceComponent } from './BluetoothDeviceComponent'; +import { bluetoothConnectDeviceThunk } from '../../../actions/bluetooth/bluetoothConnectDeviceThunk'; +import { bluetoothDisconnectDeviceThunk } from '../../../actions/bluetooth/bluetoothDisconnectDeviceThunk'; +import { closeModalApp } from '../../../actions/suite/routerActions'; +import { useDispatch } from '../../../hooks/suite'; + +const labelMap: Record = { + disconnected: 'Connect', + connecting: 'Connecting', + 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']; +const DISABLED_STATUSES: DeviceBluetoothConnectionStatusType[] = ['pairing', 'connecting']; type BluetoothDeviceItemProps = { - device: BluetoothDeviceType; - onClick: () => void; - isDisabled?: boolean; + device: BluetoothDevice; + onSelect: (id: string) => void; + uiMode: 'spatial' | 'card'; }; -export const BluetoothDeviceItem = ({ device, onClick, isDisabled }: BluetoothDeviceItemProps) => ( - - - - -); +export const BluetoothDeviceItem = ({ device, onSelect, uiMode }: BluetoothDeviceItemProps) => { + const dispatch = useDispatch(); + + const [isLoading, setIsLoading] = useState(false); + + const isDisabled = DISABLED_STATUSES.includes(device.connectionStatus.type); + const isGlobalLoading = LOADING_STATUSES.includes(device.connectionStatus.type); + + const onConnect = async () => { + onSelect(device.id); + const result = await dispatch(bluetoothConnectDeviceThunk({ id: device.id })).unwrap(); + + if (uiMode === 'card' && result.success) { + dispatch(closeModalApp()); + } + }; + + const onDisconnect = async () => { + await dispatch(bluetoothDisconnectDeviceThunk({ id: device.id })).unwrap(); + }; + + const onClickMap: Record< + DeviceBluetoothConnectionStatusType, + (() => Promise) | undefined + > = { + 'connection-error': onConnect, + 'pairing-error': undefined, + connected: onDisconnect, + connecting: undefined, + disconnected: onConnect, + paired: undefined, + pairing: undefined, + }; + + const handleOnClick = async () => { + setIsLoading(true); + await onClickMap[device.connectionStatus.type]?.(); + setIsLoading(false); + }; + + return ( + + + + + ); +}; diff --git a/packages/suite/src/components/suite/bluetooth/BluetoothDeviceList.tsx b/packages/suite/src/components/suite/bluetooth/BluetoothDeviceList.tsx index e185ebb7fed..e184cf2678c 100644 --- a/packages/suite/src/components/suite/bluetooth/BluetoothDeviceList.tsx +++ b/packages/suite/src/components/suite/bluetooth/BluetoothDeviceList.tsx @@ -1,17 +1,9 @@ -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'; import { BluetoothDeviceItem } from './BluetoothDeviceItem'; -type BluetoothDeviceListProps = { - deviceList: BluetoothDeviceState[]; - onSelect: (id: string) => void; - isScanning: boolean; - isDisabled: boolean; -}; - const SkeletonDevice = () => ( @@ -23,18 +15,28 @@ const SkeletonDevice = () => ( ); +type BluetoothDeviceListProps = { + deviceList: BluetoothDevice[]; + onSelect: (id: string) => void; + isScanning: boolean; + isDisabled: boolean; + uiMode: 'spatial' | 'card'; +}; + export const BluetoothDeviceList = ({ onSelect, deviceList, isScanning, + uiMode, }: BluetoothDeviceListProps) => ( - {deviceList.map(d => ( + {deviceList.map(device => ( onSelect(d.device.id)} + key={device.id} + device={device} + onSelect={onSelect} + uiMode={uiMode} /> ))} {isScanning && } diff --git a/packages/suite/src/components/suite/bluetooth/BluetoothSelectedDevice.tsx b/packages/suite/src/components/suite/bluetooth/BluetoothSelectedDevice.tsx index 333c0fe9f06..1cad66dce34 100644 --- a/packages/suite/src/components/suite/bluetooth/BluetoothSelectedDevice.tsx +++ b/packages/suite/src/components/suite/bluetooth/BluetoothSelectedDevice.tsx @@ -1,5 +1,16 @@ -import { BluetoothDeviceState } from '@suite-common/bluetooth'; -import { Card, ElevationContext, Icon, Row, Spinner, Text } from '@trezor/components'; +import { ReactNode } from 'react'; + +import { DeviceBluetoothConnectionStatusType } from '@suite-common/bluetooth'; +import { + Button, + Card, + Column, + ElevationContext, + Icon, + Row, + Spinner, + Text, +} from '@trezor/components'; import { spacings } from '@trezor/theme'; import { BluetoothDevice } from '@trezor/transport-bluetooth'; @@ -7,60 +18,102 @@ import { BluetoothDeviceComponent } from './BluetoothDeviceComponent'; import { BluetoothTips } from './BluetoothTips'; const PairedComponent = () => ( - - {/* Todo: here we shall solve how to continue with Trezor Host Protocol */} + Paired ); const PairingComponent = () => ( - - + + Pairing ); +const ConnectingComponent = () => ( + + + Connecting + +); + +const ConnectedComponent = () => ( + + + Connected + +); + export type OkComponentProps = { - device: BluetoothDeviceState; + device: BluetoothDevice; + onCancel: () => void; }; -const OkComponent = ({ device }: OkComponentProps) => ( - - +const OkComponent = ({ device, onCancel }: OkComponentProps) => { + const CancelButton = () => ( + + ); - {device?.status?.type === 'connected' ? : } - -); + const map: Record = { + 'connection-error': 'Connection failed', // Shall not be shown in the UI + 'pairing-error': 'Pairing failed', // Shall not be shown in the UI + disconnected: 'Disconnected', // Shall not be shown in the UI + connecting: ( + <> + + + + ), + connected: , + pairing: ( + <> + + + + ), + paired: , + }; + + return ( + + + + + {map[device.connectionStatus.type]} + + + ); +}; export type ErrorComponentProps = { - device: BluetoothDeviceState; + device: BluetoothDevice; onReScanClick: () => void; }; -const ErrorComponent = ({ device, onReScanClick }: ErrorComponentProps) => { - if (device?.status?.type !== 'error') { - return null; - } - - return ; -}; +const ErrorComponent = ({ device, onReScanClick }: ErrorComponentProps) => ( + +); export type BluetoothSelectedDeviceProps = { - device: BluetoothDeviceState; + device: BluetoothDevice; onReScanClick: () => void; + onCancel: () => void; }; export const BluetoothSelectedDevice = ({ device, onReScanClick, + onCancel, }: BluetoothSelectedDeviceProps) => ( - {device?.status?.type === 'error' ? ( + {device.connectionStatus.type === 'connection-error' ? ( ) : ( - + )} diff --git a/packages/suite/src/components/suite/bluetooth/BluetoothTips.tsx b/packages/suite/src/components/suite/bluetooth/BluetoothTips.tsx index 1b08a63b5d8..25707b36c1e 100644 --- a/packages/suite/src/components/suite/bluetooth/BluetoothTips.tsx +++ b/packages/suite/src/components/suite/bluetooth/BluetoothTips.tsx @@ -2,6 +2,7 @@ import { ReactNode } from 'react'; import { Button, Card, Column, Divider, Icon, IconName, Row, Text } from '@trezor/components'; import { spacings } from '@trezor/theme'; +import { BluetoothDevice } from '@trezor/transport-bluetooth'; type BluetoothTipProps = { icon: IconName; @@ -24,13 +25,21 @@ const BluetoothTip = ({ icon, header, text }: BluetoothTipProps) => ( type BluetoothTipsProps = { onReScanClick: () => void; header: ReactNode; + device?: BluetoothDevice; }; -export const BluetoothTips = ({ onReScanClick, header }: BluetoothTipsProps) => ( +export const BluetoothTips = ({ onReScanClick, header, device }: BluetoothTipsProps) => ( - {header} + + {header}{' '} + {device !== undefined && + (device.connectionStatus.type === 'connection-error' || + device.connectionStatus.type === 'pairing-error') && ( +
({device.connectionStatus.error})
+ )} +
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()); } From 7ea22945cfbde24b791526d4c649626bcc0e96ff Mon Sep 17 00:00:00 2001 From: Peter Sanderson Date: Thu, 27 Feb 2025 12:55:33 +0100 Subject: [PATCH 4/5] WIP: temporarily disable 30sec filtering, as timestamp providing from IPC is probably bugged --- .../suite/bluetooth/BluetoothConnect.tsx | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/suite/src/components/suite/bluetooth/BluetoothConnect.tsx b/packages/suite/src/components/suite/bluetooth/BluetoothConnect.tsx index 56215e5d68a..e5c983c6d84 100644 --- a/packages/suite/src/components/suite/bluetooth/BluetoothConnect.tsx +++ b/packages/suite/src/components/suite/bluetooth/BluetoothConnect.tsx @@ -50,16 +50,17 @@ export const BluetoothConnect = ({ onClose, uiMode }: BluetoothConnectProps) => console.log('allDevices', allDevices); - const devices = allDevices.filter(it => { - const isDeviceUnresponsiveForTooLong = - it.lastUpdatedTimestamp < lasUpdatedBoundaryTimestamp; - - if (isDeviceUnresponsiveForTooLong) { - return knownDevices.find(knownDevice => knownDevice.id === it.id) !== undefined; - } - - return true; - }); + const devices = allDevices; + // .filter(it => { + // const isDeviceUnresponsiveForTooLong = + // it.lastUpdatedTimestamp < lasUpdatedBoundaryTimestamp; + // + // if (isDeviceUnresponsiveForTooLong) { + // return knownDevices.find(knownDevice => knownDevice.id === it.id) !== undefined; + // } + // + // return true; + // }); const selectedDevice = selectedDeviceId !== null From ba961f1fec90d7c29a676765a6468f68fbb691ae Mon Sep 17 00:00:00 2001 From: Peter Sanderson Date: Thu, 27 Feb 2025 14:57:18 +0100 Subject: [PATCH 5/5] fix: wip UI --- .../bluetooth/bluetoothConnectDeviceThunk.ts | 3 +- .../bluetoothDisconnectDeviceThunk.ts | 1 + .../suite/bluetooth/BluetoothConnect.tsx | 1 + .../bluetooth/BluetoothDeviceComponent.tsx | 28 +++++++++++++++++-- .../suite/bluetooth/BluetoothDeviceItem.tsx | 12 +++++++- .../suite/bluetooth/BluetoothDeviceList.tsx | 3 ++ .../bluetooth/BluetoothSelectedDevice.tsx | 16 +++++------ 7 files changed, 50 insertions(+), 14 deletions(-) diff --git a/packages/suite/src/actions/bluetooth/bluetoothConnectDeviceThunk.ts b/packages/suite/src/actions/bluetooth/bluetoothConnectDeviceThunk.ts index a92e2df9222..2612454b630 100644 --- a/packages/suite/src/actions/bluetooth/bluetoothConnectDeviceThunk.ts +++ b/packages/suite/src/actions/bluetooth/bluetoothConnectDeviceThunk.ts @@ -15,10 +15,9 @@ export const bluetoothConnectDeviceThunk = createThunk< `${BLUETOOTH_PREFIX}/bluetoothConnectDeviceThunk`, async ({ id }, { fulfillWithValue, dispatch }) => { const result = await bluetoothIpc.connectDevice(id); + console.log('_____calling: bluetoothIpc.connectDevice(id)', id); if (!result.success) { - console.log('_______bluetoothConnectDeviceThunk :: result', result); - dispatch( notificationsActions.addToast({ type: 'error', diff --git a/packages/suite/src/actions/bluetooth/bluetoothDisconnectDeviceThunk.ts b/packages/suite/src/actions/bluetooth/bluetoothDisconnectDeviceThunk.ts index cf4a878a3dc..c27de86b024 100644 --- a/packages/suite/src/actions/bluetooth/bluetoothDisconnectDeviceThunk.ts +++ b/packages/suite/src/actions/bluetooth/bluetoothDisconnectDeviceThunk.ts @@ -14,6 +14,7 @@ export const bluetoothDisconnectDeviceThunk = createThunk< >( `${BLUETOOTH_PREFIX}/bluetoothConnectDeviceThunk`, async ({ id }, { fulfillWithValue, dispatch }) => { + console.log('_____calling: bluetoothIpc.disconnectDevice(id)', id); const result = await bluetoothIpc.disconnectDevice(id); if (!result.success) { diff --git a/packages/suite/src/components/suite/bluetooth/BluetoothConnect.tsx b/packages/suite/src/components/suite/bluetooth/BluetoothConnect.tsx index e5c983c6d84..4c43a263886 100644 --- a/packages/suite/src/components/suite/bluetooth/BluetoothConnect.tsx +++ b/packages/suite/src/components/suite/bluetooth/BluetoothConnect.tsx @@ -157,6 +157,7 @@ export const BluetoothConnect = ({ onClose, uiMode }: BluetoothConnectProps) => { + const [secAgo, setSecAgo] = useState(0); + + useEffect(() => { + setSecAgo(Math.floor((Date.now() - timestamp) / 1000)); + const interval = setInterval(() => setSecAgo(t => t + 1), 1000); + + return () => clearInterval(interval); + }, [timestamp]); + + return ( + <> + {secAgo}s ago + + ); +}; + type BluetoothDeviceProps = { device: BluetoothDevice; flex?: FlexProps['flex']; @@ -23,7 +42,7 @@ export const BluetoothDeviceComponent = ({ device, flex, margin }: BluetoothDevi const model = getModelEnumFromBytesUtil(device.data[2]); const color = getColorEnumFromVariantBytesUtil(device.data[1]); - const colorName = models[model].colors[color.toString()]; + const colorName = color !== undefined ? models[model].colors[color.toString()] : ''; return ( @@ -36,8 +55,11 @@ export const BluetoothDeviceComponent = ({ device, flex, margin }: BluetoothDevi Trezor Safe 7 - -
{device.macAddress}
+ + +
{device.macAddress}
+
+
diff --git a/packages/suite/src/components/suite/bluetooth/BluetoothDeviceItem.tsx b/packages/suite/src/components/suite/bluetooth/BluetoothDeviceItem.tsx index 15c5d047786..46d752448c4 100644 --- a/packages/suite/src/components/suite/bluetooth/BluetoothDeviceItem.tsx +++ b/packages/suite/src/components/suite/bluetooth/BluetoothDeviceItem.tsx @@ -27,10 +27,16 @@ const DISABLED_STATUSES: DeviceBluetoothConnectionStatusType[] = ['pairing', 'co type BluetoothDeviceItemProps = { device: BluetoothDevice; onSelect: (id: string) => void; + onError: () => void; uiMode: 'spatial' | 'card'; }; -export const BluetoothDeviceItem = ({ device, onSelect, uiMode }: BluetoothDeviceItemProps) => { +export const BluetoothDeviceItem = ({ + device, + onSelect, + onError, + uiMode, +}: BluetoothDeviceItemProps) => { const dispatch = useDispatch(); const [isLoading, setIsLoading] = useState(false); @@ -45,6 +51,10 @@ export const BluetoothDeviceItem = ({ device, onSelect, uiMode }: BluetoothDevic if (uiMode === 'card' && result.success) { dispatch(closeModalApp()); } + + if (!result.success) { + onError(); + } }; const onDisconnect = async () => { diff --git a/packages/suite/src/components/suite/bluetooth/BluetoothDeviceList.tsx b/packages/suite/src/components/suite/bluetooth/BluetoothDeviceList.tsx index e184cf2678c..d21b9cb3d76 100644 --- a/packages/suite/src/components/suite/bluetooth/BluetoothDeviceList.tsx +++ b/packages/suite/src/components/suite/bluetooth/BluetoothDeviceList.tsx @@ -18,6 +18,7 @@ const SkeletonDevice = () => ( type BluetoothDeviceListProps = { deviceList: BluetoothDevice[]; onSelect: (id: string) => void; + onError: () => void; isScanning: boolean; isDisabled: boolean; uiMode: 'spatial' | 'card'; @@ -25,6 +26,7 @@ type BluetoothDeviceListProps = { export const BluetoothDeviceList = ({ onSelect, + onError, deviceList, isScanning, uiMode, @@ -36,6 +38,7 @@ export const BluetoothDeviceList = ({ key={device.id} device={device} onSelect={onSelect} + onError={onError} uiMode={uiMode} /> ))} diff --git a/packages/suite/src/components/suite/bluetooth/BluetoothSelectedDevice.tsx b/packages/suite/src/components/suite/bluetooth/BluetoothSelectedDevice.tsx index 1cad66dce34..2ba0d559b1a 100644 --- a/packages/suite/src/components/suite/bluetooth/BluetoothSelectedDevice.tsx +++ b/packages/suite/src/components/suite/bluetooth/BluetoothSelectedDevice.tsx @@ -58,23 +58,23 @@ const OkComponent = ({ device, onCancel }: OkComponentProps) => { ); const map: Record = { - 'connection-error': 'Connection failed', // Shall not be shown in the UI - 'pairing-error': 'Pairing failed', // Shall not be shown in the UI disconnected: 'Disconnected', // Shall not be shown in the UI - connecting: ( + pairing: ( <> - + ), - connected: , - pairing: ( + paired: , + 'pairing-error': 'Pairing failed', // Shall not be shown in the UI + connecting: ( <> - + ), - paired: , + connected: , + 'connection-error': 'Connection failed', // Shall not be shown in the UI }; return (