From aa34e303d317a36440a89dfe1efffe0f8ff3ff59 Mon Sep 17 00:00:00 2001 From: Peter Sanderson Date: Wed, 19 Feb 2025 17:43:28 +0100 Subject: [PATCH 01/12] feat: move reducers into seperate common package, create shared state for mobile --- suite-common/bluetooth/package.json | 17 ++ .../bluetooth/src/bluetoothActions.ts | 64 ++++++++ .../bluetooth/src/bluetoothReducer.ts | 148 ++++++++++++++++++ .../bluetooth/src/bluetoothSelectors.ts | 35 +++++ suite-common/bluetooth/src/index.ts | 24 +++ suite-common/bluetooth/tsconfig.json | 9 ++ 6 files changed, 297 insertions(+) create mode 100644 suite-common/bluetooth/package.json create mode 100644 suite-common/bluetooth/src/bluetoothActions.ts create mode 100644 suite-common/bluetooth/src/bluetoothReducer.ts create mode 100644 suite-common/bluetooth/src/bluetoothSelectors.ts create mode 100644 suite-common/bluetooth/src/index.ts create mode 100644 suite-common/bluetooth/tsconfig.json diff --git a/suite-common/bluetooth/package.json b/suite-common/bluetooth/package.json new file mode 100644 index 00000000000..78accc4a9b8 --- /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 00000000000..d7593fa3465 --- /dev/null +++ b/suite-common/bluetooth/src/bluetoothActions.ts @@ -0,0 +1,64 @@ +import { createAction } from '@reduxjs/toolkit'; + +import { + BluetoothDeviceCommon, + BluetoothScanStatus, + DeviceBluetoothStatus, +} from './bluetoothReducer'; + +export const BLUETOOTH_PREFIX = '@suite/bluetooth'; + +export const bluetoothAdapterEventAction = createAction( + `${BLUETOOTH_PREFIX}/adapter-event`, + ({ isPowered }: { isPowered: boolean }) => ({ payload: { isPowered } }), +); + +type BluetoothNearbyDevicesUpdateActionPayload = { + nearbyDevices: BluetoothDeviceCommon[]; +}; + +export const bluetoothNearbyDevicesUpdateAction = createAction( + `${BLUETOOTH_PREFIX}/nearby-devices-update`, + ({ nearbyDevices }: BluetoothNearbyDevicesUpdateActionPayload) => ({ + payload: { nearbyDevices }, + }), +); + +type BluetoothKnownDevicesUpdateActionPayload = { + knownDevices: BluetoothDeviceCommon[]; +}; + +export const bluetoothKnownDevicesUpdateAction = createAction( + `${BLUETOOTH_PREFIX}/known-devices-update`, + ({ knownDevices }: BluetoothKnownDevicesUpdateActionPayload) => ({ + payload: { knownDevices }, + }), +); + +export const bluetoothRemoveKnownDeviceAction = createAction( + `${BLUETOOTH_PREFIX}/remove-known-device`, + ({ id }: { id: string }) => ({ + payload: { id }, + }), +); + +export const bluetoothConnectDeviceEventAction = createAction( + `${BLUETOOTH_PREFIX}/connect-device-event`, + ({ 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, + bluetoothNearbyDevicesUpdateAction, + bluetoothConnectDeviceEventAction, + bluetoothScanStatusAction, + bluetoothKnownDevicesUpdateAction, + bluetoothRemoveKnownDeviceAction, +}; diff --git a/suite-common/bluetooth/src/bluetoothReducer.ts b/suite-common/bluetooth/src/bluetoothReducer.ts new file mode 100644 index 00000000000..40a53e85b03 --- /dev/null +++ b/suite-common/bluetooth/src/bluetoothReducer.ts @@ -0,0 +1,148 @@ +import { AnyAction, Draft } from '@reduxjs/toolkit'; + +import { createReducerWithExtraDeps } from '@suite-common/redux-utils'; +import { deviceActions } from '@suite-common/wallet-core'; + +import { + bluetoothAdapterEventAction, + bluetoothConnectDeviceEventAction, + bluetoothKnownDevicesUpdateAction, + bluetoothNearbyDevicesUpdateAction, + bluetoothRemoveKnownDeviceAction, + bluetoothScanStatusAction, +} from './bluetoothActions'; + +export type BluetoothScanStatus = 'idle' | 'running' | 'error'; + +export type DeviceBluetoothStatus = + | { type: 'pairing'; pin?: string } + | { type: 'paired' } + | { type: 'connecting' } + | { type: 'connected' } + | { + type: 'error'; + error: string; + }; + +// Do not export this outside of this suite-common package, Suite uses ist own type +// from the '@trezor/transport-bluetooth' and mobile (native) have its own type as well. +export type BluetoothDeviceCommon = { + id: string; + name: string; + data: number[]; // Todo: consider typed data-structure for this + lastUpdatedTimestamp: number; +}; + +export type DeviceBluetoothStatusType = DeviceBluetoothStatus['type']; + +export type BluetoothState = { + adapterStatus: 'unknown' | 'enabled' | 'disabled'; + scanStatus: BluetoothScanStatus; + nearbyDevices: Array<{ device: T; status: DeviceBluetoothStatus | null }>; + + // 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[]; +}; + +export type BluetoothDeviceState = { + device: T; + status: DeviceBluetoothStatus | null; +}; + +export const prepareBluetoothReducerCreator = () => { + const initialState: BluetoothState = { + adapterStatus: 'unknown', + scanStatus: 'idle', + nearbyDevices: [] as BluetoothDeviceState[], + knownDevices: [] as T[], + }; + + return createReducerWithExtraDeps>(initialState, (builder, extra) => + builder + .addCase(bluetoothAdapterEventAction, (state, { payload: { isPowered } }) => { + state.adapterStatus = isPowered ? 'enabled' : 'disabled'; + if (!isPowered) { + state.nearbyDevices = []; + state.scanStatus = 'idle'; + } + }) + .addCase( + bluetoothNearbyDevicesUpdateAction, + (state, { payload: { nearbyDevices } }) => { + state.nearbyDevices = nearbyDevices + .sort((a, b) => a.lastUpdatedTimestamp - b.lastUpdatedTimestamp) + .map( + (device): Draft> => ({ + device: device as Draft, + status: null, + }), + ); + }, + ) + .addCase( + bluetoothConnectDeviceEventAction, + (state, { payload: { id, connectionStatus } }) => { + const device = state.nearbyDevices.find(it => it.device.id === id); + + if (device !== undefined) { + device.status = connectionStatus; + } + }, + ) + .addCase(bluetoothKnownDevicesUpdateAction, (state, { payload: { knownDevices } }) => { + state.knownDevices = knownDevices as Draft[]; + }) + .addCase(bluetoothRemoveKnownDeviceAction, (state, { payload: { id } }) => { + state.knownDevices = state.knownDevices.filter( + knownDevice => knownDevice.id !== id, + ); + }) + .addCase(bluetoothScanStatusAction, (state, { payload: { status } }) => { + state.scanStatus = status; + }) + .addCase(deviceActions.deviceDisconnect, (state, { payload: { bluetoothProps } }) => { + if (bluetoothProps !== undefined) { + state.nearbyDevices = state.nearbyDevices.filter( + it => it.device.id !== bluetoothProps.id, + ); + } + }) + .addCase( + deviceActions.connectDevice, + ( + state, + { + payload: { + device: { bluetoothProps }, + }, + }, + ) => { + if (bluetoothProps === undefined) { + return; + } + + const deviceState = state.nearbyDevices.find( + it => it.device.id === bluetoothProps.id, + ); + + if (deviceState !== 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); + } + } + }, + ) + .addMatcher( + action => action.type === extra.actionTypes.storageLoad, + (state, action: AnyAction) => { + state.knownDevices = action.payload.knownDevices?.bluetooth ?? []; + }, + ), + ); +}; diff --git a/suite-common/bluetooth/src/bluetoothSelectors.ts b/suite-common/bluetooth/src/bluetoothSelectors.ts new file mode 100644 index 00000000000..dcd7d07a3e7 --- /dev/null +++ b/suite-common/bluetooth/src/bluetoothSelectors.ts @@ -0,0 +1,35 @@ +import { createWeakMapSelector } from '@suite-common/redux-utils'; + +import { BluetoothDeviceCommon, BluetoothDeviceState, BluetoothState } from './bluetoothReducer'; + +type State = { + bluetooth: BluetoothState; +}; + +export const selectAdapterStatus = (state: State) => + state.bluetooth.adapterStatus; + +export const selectKnownDevices = (state: State) => + state.bluetooth.knownDevices; + +export const prepareSelectAllDevices = () => + createWeakMapSelector.withTypes>()( + [state => state.bluetooth.nearbyDevices, state => state.bluetooth.knownDevices], + (nearbyDevices, knownDevices) => { + const map = new Map>(); + knownDevices.forEach(knownDevice => { + map.set(knownDevice.id, { device: knownDevice, status: null }); + }); + + nearbyDevices.forEach(nearbyDevice => { + if (!map.has(nearbyDevice.device.id)) { + map.set(nearbyDevice.device.id, nearbyDevice); + } + }); + + return Array.from(map.values()); + }, + ); + +export const selectScanStatus = (state: State) => + state.bluetooth.scanStatus; diff --git a/suite-common/bluetooth/src/index.ts b/suite-common/bluetooth/src/index.ts new file mode 100644 index 00000000000..dfb4615599e --- /dev/null +++ b/suite-common/bluetooth/src/index.ts @@ -0,0 +1,24 @@ +export { + bluetoothAdapterEventAction, + BLUETOOTH_PREFIX, + allBluetoothActions, + bluetoothConnectDeviceEventAction, + bluetoothNearbyDevicesUpdateAction, + bluetoothKnownDevicesUpdateAction, + bluetoothRemoveKnownDeviceAction, + bluetoothScanStatusAction, +} from './bluetoothActions'; + +export { prepareBluetoothReducerCreator } from './bluetoothReducer'; +export type { + BluetoothDeviceState, + BluetoothScanStatus, + DeviceBluetoothStatusType, +} from './bluetoothReducer'; + +export { + prepareSelectAllDevices, + selectKnownDevices, + selectAdapterStatus, + selectScanStatus, +} from './bluetoothSelectors'; diff --git a/suite-common/bluetooth/tsconfig.json b/suite-common/bluetooth/tsconfig.json new file mode 100644 index 00000000000..73c744e02f0 --- /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" } + ] +} From c0009aaa20c76ad027160e3d9a4f86710f28989a Mon Sep 17 00:00:00 2001 From: Peter Sanderson Date: Wed, 19 Feb 2025 17:43:38 +0100 Subject: [PATCH 02/12] feat: implement suite-common bluetooth code into Suite --- .../src/actions/bluetooth/bluetoothActions.ts | 39 ----- .../bluetooth/bluetoothConnectDeviceThunk.ts | 3 +- .../bluetooth/bluetoothStartScanningThunk.ts | 6 +- .../bluetooth/bluetoothStopScanningThunk.ts | 6 +- .../actions/bluetooth/initBluetoothThunk.ts | 40 +++-- .../suite/src/actions/suite/storageActions.ts | 4 +- .../suite/bluetooth/BluetoothConnect.tsx | 59 +++---- ...evice.tsx => BluetoothDeviceComponent.tsx} | 6 +- .../suite/bluetooth/BluetoothDeviceItem.tsx | 4 +- .../suite/bluetooth/BluetoothDeviceList.tsx | 5 +- .../suite/bluetooth/BluetoothPairingPin.tsx | 8 +- .../suite/bluetooth/BluetoothScanFooter.tsx | 4 +- .../bluetooth/BluetoothSelectedDevice.tsx | 19 +-- .../middlewares/wallet/storageMiddleware.ts | 6 +- .../reducers/bluetooth/bluetoothReducer.ts | 146 ------------------ .../reducers/bluetooth/bluetoothSelectors.ts | 7 - packages/suite/src/reducers/store.ts | 4 +- packages/suite/src/types/suite/index.ts | 2 +- .../SettingsDevice/BluetoothEraseBonds.tsx | 9 +- yarn.lock | 10 ++ 20 files changed, 115 insertions(+), 272 deletions(-) delete mode 100644 packages/suite/src/actions/bluetooth/bluetoothActions.ts rename packages/suite/src/components/suite/bluetooth/{BluetoothDevice.tsx => BluetoothDeviceComponent.tsx} (89%) delete mode 100644 packages/suite/src/reducers/bluetooth/bluetoothReducer.ts delete mode 100644 packages/suite/src/reducers/bluetooth/bluetoothSelectors.ts diff --git a/packages/suite/src/actions/bluetooth/bluetoothActions.ts b/packages/suite/src/actions/bluetooth/bluetoothActions.ts deleted file mode 100644 index 94457659b17..00000000000 --- a/packages/suite/src/actions/bluetooth/bluetoothActions.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { createAction } from '@reduxjs/toolkit'; - -import { BluetoothDevice } from '@trezor/transport-bluetooth'; - -import { - BluetoothScanStatus, - DeviceBluetoothStatus, -} from '../../reducers/bluetooth/bluetoothReducer'; - -export const BLUETOOTH_PREFIX = '@suite/bluetooth'; - -export const bluetoothAdapterEventAction = createAction( - `${BLUETOOTH_PREFIX}/adapter-event`, - ({ isPowered }: { isPowered: boolean }) => ({ payload: { isPowered } }), -); - -export const bluetoothDeviceListUpdate = createAction( - `${BLUETOOTH_PREFIX}/device-list-update`, - ({ devices }: { devices: BluetoothDevice[] }) => ({ payload: { devices } }), -); - -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/packages/suite/src/actions/bluetooth/bluetoothConnectDeviceThunk.ts b/packages/suite/src/actions/bluetooth/bluetoothConnectDeviceThunk.ts index b5bd33d7d3f..6ca9a7b847c 100644 --- a/packages/suite/src/actions/bluetooth/bluetoothConnectDeviceThunk.ts +++ b/packages/suite/src/actions/bluetooth/bluetoothConnectDeviceThunk.ts @@ -1,8 +1,7 @@ +import { BLUETOOTH_PREFIX } from '@suite-common/bluetooth'; import { createThunk } from '@suite-common/redux-utils'; import { bluetoothIpc } from '@trezor/transport-bluetooth'; -import { BLUETOOTH_PREFIX } from './bluetoothActions'; - type ThunkResponse = ReturnType; export const bluetoothConnectDeviceThunk = createThunk( diff --git a/packages/suite/src/actions/bluetooth/bluetoothStartScanningThunk.ts b/packages/suite/src/actions/bluetooth/bluetoothStartScanningThunk.ts index a1a389033df..90af08320ea 100644 --- a/packages/suite/src/actions/bluetooth/bluetoothStartScanningThunk.ts +++ b/packages/suite/src/actions/bluetooth/bluetoothStartScanningThunk.ts @@ -1,11 +1,11 @@ +import { BLUETOOTH_PREFIX, bluetoothScanStatusAction } from '@suite-common/bluetooth'; import { createThunk } from '@suite-common/redux-utils'; import { bluetoothIpc } from '@trezor/transport-bluetooth'; -import { BLUETOOTH_PREFIX } from './bluetoothActions'; - export const bluetoothStartScanningThunk = createThunk( `${BLUETOOTH_PREFIX}/bluetoothStartScanningThunk`, - _ => { + (_, { dispatch }) => { + dispatch(bluetoothScanStatusAction({ status: 'running' })); // This can fail, but if there is an error we already got it from `adapter-event` // and user is informed about it (bluetooth turned-off, ...) bluetoothIpc.startScan(); diff --git a/packages/suite/src/actions/bluetooth/bluetoothStopScanningThunk.ts b/packages/suite/src/actions/bluetooth/bluetoothStopScanningThunk.ts index 086f3a21959..445c92db5e9 100644 --- a/packages/suite/src/actions/bluetooth/bluetoothStopScanningThunk.ts +++ b/packages/suite/src/actions/bluetooth/bluetoothStopScanningThunk.ts @@ -1,11 +1,11 @@ +import { BLUETOOTH_PREFIX, bluetoothScanStatusAction } from '@suite-common/bluetooth'; import { createThunk } from '@suite-common/redux-utils'; import { bluetoothIpc } from '@trezor/transport-bluetooth'; -import { BLUETOOTH_PREFIX } from './bluetoothActions'; - export const bluetoothStopScanningThunk = createThunk( `${BLUETOOTH_PREFIX}/bluetoothStopScanningThunk`, - _ => { + (_, { dispatch }) => { + dispatch(bluetoothScanStatusAction({ status: 'idle' })); // This can fail, but there is nothing we can do about it bluetoothIpc.stopScan(); }, diff --git a/packages/suite/src/actions/bluetooth/initBluetoothThunk.ts b/packages/suite/src/actions/bluetooth/initBluetoothThunk.ts index 05512b97dcc..74b242988c5 100644 --- a/packages/suite/src/actions/bluetooth/initBluetoothThunk.ts +++ b/packages/suite/src/actions/bluetooth/initBluetoothThunk.ts @@ -1,13 +1,15 @@ -import { createThunk } from '@suite-common/redux-utils/'; -import { DeviceConnectionStatus, bluetoothIpc } from '@trezor/transport-bluetooth'; -import { Without } from '@trezor/type-utils'; - import { BLUETOOTH_PREFIX, bluetoothAdapterEventAction, bluetoothConnectDeviceEventAction, - bluetoothDeviceListUpdate, -} from './bluetoothActions'; + bluetoothKnownDevicesUpdateAction, + bluetoothNearbyDevicesUpdateAction, + 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 { selectSuiteFlags } from '../../reducers/suite/suiteReducer'; type DeviceConnectionStatusWithOptionalId = Without & { @@ -28,9 +30,27 @@ export const initBluetoothThunk = createThunk( dispatch(bluetoothAdapterEventAction({ isPowered })); }); - bluetoothIpc.on('device-list-update', devices => { - console.warn('device-list-update', devices); - dispatch(bluetoothDeviceListUpdate({ devices })); + bluetoothIpc.on('device-list-update', nearbyDevices => { + console.warn('device-list-update', nearbyDevices); + + const knownDevices = selectKnownDevices(getState()); + + console.log('nearbyDevices', nearbyDevices); + + // update pairedDevices, id is changed after pairing (linux) + const remappedKnownDevices = knownDevices.map(knownDevice => { + // find devices with the same address but different id + const changed = nearbyDevices.find( + nearbyDevice => + nearbyDevice.address === knownDevice.address && + nearbyDevice.id !== knownDevice.id, + ); + + return changed ? { ...knownDevice, id: changed.id } : knownDevice; + }); + + dispatch(bluetoothKnownDevicesUpdateAction({ knownDevices: remappedKnownDevices })); + dispatch(bluetoothNearbyDevicesUpdateAction({ nearbyDevices })); }); bluetoothIpc.on('device-connection-status', connectionStatus => { @@ -49,7 +69,7 @@ export const initBluetoothThunk = createThunk( }); // TODO: this should be called after trezor/connect init? - const knownDevices = getState().bluetooth.pairedDevices; + const knownDevices = selectKnownDevices(getState()); await bluetoothIpc.init({ knownDevices }); }, ); diff --git a/packages/suite/src/actions/suite/storageActions.ts b/packages/suite/src/actions/suite/storageActions.ts index 1ec655fb545..85d641a29fe 100644 --- a/packages/suite/src/actions/suite/storageActions.ts +++ b/packages/suite/src/actions/suite/storageActions.ts @@ -104,8 +104,8 @@ export const saveCoinjoinDebugSettings = () => async (_dispatch: Dispatch, getSt export const saveKnownDevices = () => async (_dispatch: Dispatch, getState: GetState) => { if (!(await db.isAccessible())) return; - const { pairedDevices } = getState().bluetooth; - db.addItem('knownDevices', { bluetooth: pairedDevices }, 'devices', true); + const { knownDevices } = getState().bluetooth; + db.addItem('knownDevices', { bluetooth: knownDevices }, 'devices', true); }; export const saveFormDraft = async (key: string, draft: FieldValues) => { diff --git a/packages/suite/src/components/suite/bluetooth/BluetoothConnect.tsx b/packages/suite/src/components/suite/bluetooth/BluetoothConnect.tsx index de9f0f7fa69..85d567312e8 100644 --- a/packages/suite/src/components/suite/bluetooth/BluetoothConnect.tsx +++ b/packages/suite/src/components/suite/bluetooth/BluetoothConnect.tsx @@ -1,9 +1,16 @@ import { useCallback, useEffect, useState } from 'react'; +import { + bluetoothConnectDeviceEventAction, + bluetoothScanStatusAction, + prepareSelectAllDevices, + selectAdapterStatus, + selectScanStatus, +} from '@suite-common/bluetooth'; import { notificationsActions } from '@suite-common/toast-notifications'; import { Card, Column, ElevationUp } from '@trezor/components'; -import TrezorConnect from '@trezor/connect'; import { spacings } from '@trezor/theme'; +import { BluetoothDevice } from '@trezor/transport-bluetooth'; import { TimerId } from '@trezor/type-utils'; import { BluetoothDeviceList } from './BluetoothDeviceList'; @@ -14,19 +21,10 @@ import { BluetoothSelectedDevice } from './BluetoothSelectedDevice'; import { BluetoothTips } from './BluetoothTips'; import { BluetoothNotEnabled } from './errors/BluetoothNotEnabled'; import { BluetoothVersionNotCompatible } from './errors/BluetoothVersionNotCompatible'; -import { - bluetoothConnectDeviceEventAction, - bluetoothScanStatusAction, -} from '../../../actions/bluetooth/bluetoothActions'; import { bluetoothConnectDeviceThunk } from '../../../actions/bluetooth/bluetoothConnectDeviceThunk'; import { bluetoothStartScanningThunk } from '../../../actions/bluetooth/bluetoothStartScanningThunk'; import { bluetoothStopScanningThunk } from '../../../actions/bluetooth/bluetoothStopScanningThunk'; import { useDispatch, useSelector } from '../../../hooks/suite'; -import { - selectBluetoothDeviceList, - selectBluetoothEnabled, - selectBluetoothScanStatus, -} from '../../../reducers/bluetooth/bluetoothSelectors'; const SCAN_TIMEOUT = 30_000; @@ -35,17 +33,21 @@ type BluetoothConnectProps = { uiMode: 'spatial' | 'card'; }; +const selectAllDevices = prepareSelectAllDevices(); + export const BluetoothConnect = ({ onClose, uiMode }: BluetoothConnectProps) => { const dispatch = useDispatch(); const [selectedDeviceId, setSelectedDeviceId] = useState(null); const [scannerTimerId, setScannerTimerId] = useState(null); - const isBluetoothEnabled = useSelector(selectBluetoothEnabled); - const scanStatus = useSelector(selectBluetoothScanStatus); - const deviceList = useSelector(selectBluetoothDeviceList); - const devices = Object.values(deviceList); + const bluetoothAdapterStatus = useSelector(selectAdapterStatus); + const scanStatus = useSelector(selectScanStatus); + const devices = useSelector(selectAllDevices); - const selectedDevice = selectedDeviceId !== null ? deviceList[selectedDeviceId] ?? null : null; + const selectedDevice = + selectedDeviceId !== null + ? devices.find(device => device.device.id === selectedDeviceId) + : undefined; useEffect(() => { dispatch(bluetoothStartScanningThunk()); @@ -64,7 +66,7 @@ export const BluetoothConnect = ({ onClose, uiMode }: BluetoothConnectProps) => useEffect(() => { // Intentionally no `clearScamTimer`, this is first run and if we use this we would create infinite re-render const timerId = setTimeout(() => { - dispatch(bluetoothScanStatusAction({ status: 'done' })); + dispatch(bluetoothScanStatusAction({ status: 'idle' })); }, SCAN_TIMEOUT); setScannerTimerId(timerId); @@ -76,14 +78,13 @@ export const BluetoothConnect = ({ onClose, uiMode }: BluetoothConnectProps) => clearScamTimer(); const timerId = setTimeout(() => { - dispatch(bluetoothScanStatusAction({ status: 'done' })); + dispatch(bluetoothScanStatusAction({ status: 'idle' })); }, SCAN_TIMEOUT); setScannerTimerId(timerId); }; const onSelect = async (id: string) => { setSelectedDeviceId(id); - const result = await dispatch(bluetoothConnectDeviceThunk({ id })).unwrap(); if (!result.success) { @@ -100,27 +101,16 @@ export const BluetoothConnect = ({ onClose, uiMode }: BluetoothConnectProps) => }), ); } else { - // Todo: What to do with error in this flow? UI-Wise - dispatch( bluetoothConnectDeviceEventAction({ id, connectionStatus: { type: 'connected' }, }), ); - - // WAIT for connect event, TODO: figure out better way - const closePopupAfterConnection = () => { - TrezorConnect.off('device-connect', closePopupAfterConnection); - TrezorConnect.off('device-connect_unacquired', closePopupAfterConnection); - // setSelectedDeviceStatus({ type: 'error', id }); // Todo: what here? - }; - TrezorConnect.on('device-connect', closePopupAfterConnection); - TrezorConnect.on('device-connect_unacquired', closePopupAfterConnection); } }; - if (!isBluetoothEnabled) { + if (bluetoothAdapterStatus === 'disabled') { return ; } @@ -133,8 +123,8 @@ export const BluetoothConnect = ({ onClose, uiMode }: BluetoothConnectProps) => console.log('selectedDevice', selectedDevice); // This is fake, we scan for devices all the time - const isScanning = scanStatus !== 'done'; - const scanFailed = devices.length === 0 && scanStatus === 'done'; + const isScanning = scanStatus === 'running'; + const scanFailed = devices.length === 0 && scanStatus === 'idle'; const handlePairingCancel = () => { setSelectedDeviceId(null); @@ -142,7 +132,8 @@ export const BluetoothConnect = ({ onClose, uiMode }: BluetoothConnectProps) => }; if ( - selectedDevice !== null && + selectedDevice !== undefined && + selectedDevice.status !== null && selectedDevice.status.type === 'pairing' && (selectedDevice.status.pin?.length ?? 0) > 0 ) { @@ -155,7 +146,7 @@ export const BluetoothConnect = ({ onClose, uiMode }: BluetoothConnectProps) => ); } - if (selectedDevice !== null) { + if (selectedDevice !== undefined) { return ; } diff --git a/packages/suite/src/components/suite/bluetooth/BluetoothDevice.tsx b/packages/suite/src/components/suite/bluetooth/BluetoothDeviceComponent.tsx similarity index 89% rename from packages/suite/src/components/suite/bluetooth/BluetoothDevice.tsx rename to packages/suite/src/components/suite/bluetooth/BluetoothDeviceComponent.tsx index 5d093829364..91c9e0319cc 100644 --- a/packages/suite/src/components/suite/bluetooth/BluetoothDevice.tsx +++ b/packages/suite/src/components/suite/bluetooth/BluetoothDeviceComponent.tsx @@ -3,10 +3,10 @@ import { DeviceModelInternal } from '@trezor/connect'; import { models } from '@trezor/connect/src/data/models'; // Todo: solve this import issue import { RotateDeviceImage } from '@trezor/product-components'; import { spacings } from '@trezor/theme'; -import { BluetoothDevice as BluetoothDeviceType } from '@trezor/transport-bluetooth'; +import { BluetoothDevice } from '@trezor/transport-bluetooth'; type BluetoothDeviceProps = { - device: BluetoothDeviceType; + device: BluetoothDevice; flex?: FlexProps['flex']; margin?: FlexProps['margin']; }; @@ -18,7 +18,7 @@ const getModelEnumFromBytesUtil = (_id: number) => DeviceModelInternal.T3W1; // discuss final format of it const getColorEnumFromVariantBytesUtil = (variant: number) => variant; -export const BluetoothDevice = ({ device, flex, margin }: BluetoothDeviceProps) => { +export const BluetoothDeviceComponent = ({ device, flex, margin }: BluetoothDeviceProps) => { const model = getModelEnumFromBytesUtil(device.data[2]); const color = getColorEnumFromVariantBytesUtil(device.data[1]); const colorName = models[model].colors[color.toString()]; diff --git a/packages/suite/src/components/suite/bluetooth/BluetoothDeviceItem.tsx b/packages/suite/src/components/suite/bluetooth/BluetoothDeviceItem.tsx index e1234e1394b..082fabdac1f 100644 --- a/packages/suite/src/components/suite/bluetooth/BluetoothDeviceItem.tsx +++ b/packages/suite/src/components/suite/bluetooth/BluetoothDeviceItem.tsx @@ -2,7 +2,7 @@ import { Button, Row } from '@trezor/components'; import { spacings } from '@trezor/theme'; import { BluetoothDevice as BluetoothDeviceType } from '@trezor/transport-bluetooth'; -import { BluetoothDevice } from './BluetoothDevice'; +import { BluetoothDeviceComponent } from './BluetoothDeviceComponent'; type BluetoothDeviceItemProps = { device: BluetoothDeviceType; @@ -12,7 +12,7 @@ type BluetoothDeviceItemProps = { export const BluetoothDeviceItem = ({ device, onClick, isDisabled }: BluetoothDeviceItemProps) => ( - + From c9d03f1c53bd912f59473ec39ffee6b69c044802 Mon Sep 17 00:00:00 2001 From: Peter Sanderson Date: Fri, 21 Feb 2025 13:47:30 +0100 Subject: [PATCH 09/12] fixup! feat: move reducers into seperate common package, create shared state for mobile --- suite-common/bluetooth/src/bluetoothSelectors.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/suite-common/bluetooth/src/bluetoothSelectors.ts b/suite-common/bluetooth/src/bluetoothSelectors.ts index 96007ed5ea4..8e952616ec8 100644 --- a/suite-common/bluetooth/src/bluetoothSelectors.ts +++ b/suite-common/bluetooth/src/bluetoothSelectors.ts @@ -13,6 +13,14 @@ export const selectAdapterStatus = ( export const selectKnownDevices = (state: WithBluetoothState) => state.bluetooth.knownDevices; +/** + * We need to have generic `createWeakMapSelector.withTypes` so we need to wrap it into Higher Order Function, + * but we need to make sure it is called only **once** to not break the memoization. So it is named + * `prepareSelectAllDevices` and shall be called **outside** of the component to create a selector with + * concrete type: + * + * For example: `const selectAllDevices = prepareSelectAllDevices();` + */ export const prepareSelectAllDevices = () => createWeakMapSelector.withTypes>()( [state => state.bluetooth.nearbyDevices, state => state.bluetooth.knownDevices], From 9dcc1e1826b25145cd16dff528b5fe8070021979 Mon Sep 17 00:00:00 2001 From: Peter Sanderson Date: Fri, 21 Feb 2025 15:48:27 +0100 Subject: [PATCH 10/12] fixup! feat: move reducers into seperate common package, create shared state for mobile --- .../bluetooth/src/bluetoothActions.ts | 26 ++++++++--------- .../bluetooth/src/bluetoothReducer.ts | 28 ++++++++----------- suite-common/bluetooth/src/index.ts | 11 +------- .../bluetooth/tests/bluetoothReducer.test.ts | 24 ++++++---------- 4 files changed, 35 insertions(+), 54 deletions(-) diff --git a/suite-common/bluetooth/src/bluetoothActions.ts b/suite-common/bluetooth/src/bluetoothActions.ts index d7593fa3465..bf43e7a7ec2 100644 --- a/suite-common/bluetooth/src/bluetoothActions.ts +++ b/suite-common/bluetooth/src/bluetoothActions.ts @@ -8,7 +8,7 @@ import { export const BLUETOOTH_PREFIX = '@suite/bluetooth'; -export const bluetoothAdapterEventAction = createAction( +const adapterEventAction = createAction( `${BLUETOOTH_PREFIX}/adapter-event`, ({ isPowered }: { isPowered: boolean }) => ({ payload: { isPowered } }), ); @@ -17,7 +17,7 @@ type BluetoothNearbyDevicesUpdateActionPayload = { nearbyDevices: BluetoothDeviceCommon[]; }; -export const bluetoothNearbyDevicesUpdateAction = createAction( +const nearbyDevicesUpdateAction = createAction( `${BLUETOOTH_PREFIX}/nearby-devices-update`, ({ nearbyDevices }: BluetoothNearbyDevicesUpdateActionPayload) => ({ payload: { nearbyDevices }, @@ -28,37 +28,37 @@ type BluetoothKnownDevicesUpdateActionPayload = { knownDevices: BluetoothDeviceCommon[]; }; -export const bluetoothKnownDevicesUpdateAction = createAction( +const knownDevicesUpdateAction = createAction( `${BLUETOOTH_PREFIX}/known-devices-update`, ({ knownDevices }: BluetoothKnownDevicesUpdateActionPayload) => ({ payload: { knownDevices }, }), ); -export const bluetoothRemoveKnownDeviceAction = createAction( +const removeKnownDeviceAction = createAction( `${BLUETOOTH_PREFIX}/remove-known-device`, ({ id }: { id: string }) => ({ payload: { id }, }), ); -export const bluetoothConnectDeviceEventAction = createAction( +const connectDeviceEventAction = createAction( `${BLUETOOTH_PREFIX}/connect-device-event`, ({ connectionStatus, id }: { id: string; connectionStatus: DeviceBluetoothStatus }) => ({ payload: { id, connectionStatus }, }), ); -export const bluetoothScanStatusAction = createAction( +const scanStatusAction = createAction( `${BLUETOOTH_PREFIX}/scan-status`, ({ status }: { status: BluetoothScanStatus }) => ({ payload: { status } }), ); -export const allBluetoothActions = { - bluetoothAdapterEventAction, - bluetoothNearbyDevicesUpdateAction, - bluetoothConnectDeviceEventAction, - bluetoothScanStatusAction, - bluetoothKnownDevicesUpdateAction, - bluetoothRemoveKnownDeviceAction, +export const bluetoothActions = { + adapterEventAction, + nearbyDevicesUpdateAction, + connectDeviceEventAction, + scanStatusAction, + knownDevicesUpdateAction, + removeKnownDeviceAction, }; diff --git a/suite-common/bluetooth/src/bluetoothReducer.ts b/suite-common/bluetooth/src/bluetoothReducer.ts index bb72d4e6da3..b092ad39093 100644 --- a/suite-common/bluetooth/src/bluetoothReducer.ts +++ b/suite-common/bluetooth/src/bluetoothReducer.ts @@ -3,14 +3,7 @@ import { AnyAction, Draft } from '@reduxjs/toolkit'; import { createReducerWithExtraDeps } from '@suite-common/redux-utils'; import { deviceActions } from '@suite-common/wallet-core'; -import { - bluetoothAdapterEventAction, - bluetoothConnectDeviceEventAction, - bluetoothKnownDevicesUpdateAction, - bluetoothNearbyDevicesUpdateAction, - bluetoothRemoveKnownDeviceAction, - bluetoothScanStatusAction, -} from './bluetoothActions'; +import { bluetoothActions } from './bluetoothActions'; export type BluetoothScanStatus = 'idle' | 'running' | 'error'; @@ -60,7 +53,7 @@ export const prepareBluetoothReducerCreator = ( return createReducerWithExtraDeps>(initialState, (builder, extra) => builder - .addCase(bluetoothAdapterEventAction, (state, { payload: { isPowered } }) => { + .addCase(bluetoothActions.adapterEventAction, (state, { payload: { isPowered } }) => { state.adapterStatus = isPowered ? 'enabled' : 'disabled'; if (!isPowered) { state.nearbyDevices = []; @@ -68,7 +61,7 @@ export const prepareBluetoothReducerCreator = ( } }) .addCase( - bluetoothNearbyDevicesUpdateAction, + bluetoothActions.nearbyDevicesUpdateAction, (state, { payload: { nearbyDevices } }) => { state.nearbyDevices = nearbyDevices .sort((a, b) => b.lastUpdatedTimestamp - a.lastUpdatedTimestamp) @@ -83,7 +76,7 @@ export const prepareBluetoothReducerCreator = ( }, ) .addCase( - bluetoothConnectDeviceEventAction, + bluetoothActions.connectDeviceEventAction, (state, { payload: { id, connectionStatus } }) => { const device = state.nearbyDevices.find(it => it.device.id === id); @@ -92,15 +85,18 @@ export const prepareBluetoothReducerCreator = ( } }, ) - .addCase(bluetoothKnownDevicesUpdateAction, (state, { payload: { knownDevices } }) => { - state.knownDevices = knownDevices as Draft[]; - }) - .addCase(bluetoothRemoveKnownDeviceAction, (state, { payload: { id } }) => { + .addCase( + bluetoothActions.knownDevicesUpdateAction, + (state, { payload: { knownDevices } }) => { + state.knownDevices = knownDevices as Draft[]; + }, + ) + .addCase(bluetoothActions.removeKnownDeviceAction, (state, { payload: { id } }) => { state.knownDevices = state.knownDevices.filter( knownDevice => knownDevice.id !== id, ); }) - .addCase(bluetoothScanStatusAction, (state, { payload: { status } }) => { + .addCase(bluetoothActions.scanStatusAction, (state, { payload: { status } }) => { state.scanStatus = status; }) .addCase(deviceActions.deviceDisconnect, (state, { payload: { bluetoothProps } }) => { diff --git a/suite-common/bluetooth/src/index.ts b/suite-common/bluetooth/src/index.ts index dfb4615599e..7fe15e21c01 100644 --- a/suite-common/bluetooth/src/index.ts +++ b/suite-common/bluetooth/src/index.ts @@ -1,13 +1,4 @@ -export { - bluetoothAdapterEventAction, - BLUETOOTH_PREFIX, - allBluetoothActions, - bluetoothConnectDeviceEventAction, - bluetoothNearbyDevicesUpdateAction, - bluetoothKnownDevicesUpdateAction, - bluetoothRemoveKnownDeviceAction, - bluetoothScanStatusAction, -} from './bluetoothActions'; +export { BLUETOOTH_PREFIX, bluetoothActions } from './bluetoothActions'; export { prepareBluetoothReducerCreator } from './bluetoothReducer'; export type { diff --git a/suite-common/bluetooth/tests/bluetoothReducer.test.ts b/suite-common/bluetooth/tests/bluetoothReducer.test.ts index aa9804eacc7..b6a0af53c30 100644 --- a/suite-common/bluetooth/tests/bluetoothReducer.test.ts +++ b/suite-common/bluetooth/tests/bluetoothReducer.test.ts @@ -5,15 +5,7 @@ import { configureMockStore, extraDependenciesMock } from '@suite-common/test-ut import { deviceActions } from '@suite-common/wallet-core'; import { Device } from '@trezor/connect'; -import { - BluetoothDeviceState, - bluetoothAdapterEventAction, - bluetoothConnectDeviceEventAction, - bluetoothKnownDevicesUpdateAction, - bluetoothNearbyDevicesUpdateAction, - bluetoothRemoveKnownDeviceAction, - prepareBluetoothReducerCreator, -} from '../src'; +import { BluetoothDeviceState, bluetoothActions, prepareBluetoothReducerCreator } from '../src'; import { BluetoothDeviceCommon, BluetoothState } from '../src/bluetoothReducer'; const bluetoothReducer = @@ -65,9 +57,9 @@ describe('bluetoothReducer', () => { }); expect(store.getState().bluetooth.adapterStatus).toEqual('unknown'); - store.dispatch(bluetoothAdapterEventAction({ isPowered: true })); + store.dispatch(bluetoothActions.adapterEventAction({ isPowered: true })); expect(store.getState().bluetooth.adapterStatus).toEqual('enabled'); - store.dispatch(bluetoothAdapterEventAction({ isPowered: false })); + store.dispatch(bluetoothActions.adapterEventAction({ isPowered: false })); expect(store.getState().bluetooth.adapterStatus).toEqual('disabled'); }); @@ -88,7 +80,7 @@ describe('bluetoothReducer', () => { bluetoothStateDeviceC.device, ]; - store.dispatch(bluetoothNearbyDevicesUpdateAction({ nearbyDevices })); + store.dispatch(bluetoothActions.nearbyDevicesUpdateAction({ nearbyDevices })); expect(store.getState().bluetooth.nearbyDevices).toEqual([ bluetoothStateDeviceC, // No `B` device present, it was dropped @@ -109,7 +101,7 @@ describe('bluetoothReducer', () => { }); store.dispatch( - bluetoothConnectDeviceEventAction({ + bluetoothActions.connectDeviceEventAction({ id: 'A', connectionStatus: { type: 'pairing', pin: '12345' }, }), @@ -134,10 +126,12 @@ describe('bluetoothReducer', () => { bluetoothStateDeviceB.device, ]; - store.dispatch(bluetoothKnownDevicesUpdateAction({ knownDevices: knownDeviceToAdd })); + store.dispatch( + bluetoothActions.knownDevicesUpdateAction({ knownDevices: knownDeviceToAdd }), + ); expect(store.getState().bluetooth.knownDevices).toEqual(knownDeviceToAdd); - store.dispatch(bluetoothRemoveKnownDeviceAction({ id: 'A' })); + store.dispatch(bluetoothActions.removeKnownDeviceAction({ id: 'A' })); expect(store.getState().bluetooth.knownDevices).toEqual([bluetoothStateDeviceB.device]); }); From e0b0c76ef47ddad87b5f13e9f62346e7e209f026 Mon Sep 17 00:00:00 2001 From: Peter Sanderson Date: Fri, 21 Feb 2025 15:49:20 +0100 Subject: [PATCH 11/12] fixup! feat: implement suite-common bluetooth code into Suite --- .../bluetooth/bluetoothStartScanningThunk.ts | 4 ++-- .../bluetooth/bluetoothStopScanningThunk.ts | 4 ++-- .../actions/bluetooth/initBluetoothThunk.ts | 19 +++++++------------ .../suite/bluetooth/BluetoothConnect.tsx | 13 ++++++------- .../middlewares/wallet/storageMiddleware.ts | 4 ++-- packages/suite/src/types/suite/index.ts | 4 ++-- .../SettingsDevice/BluetoothEraseBonds.tsx | 4 ++-- 7 files changed, 23 insertions(+), 29 deletions(-) diff --git a/packages/suite/src/actions/bluetooth/bluetoothStartScanningThunk.ts b/packages/suite/src/actions/bluetooth/bluetoothStartScanningThunk.ts index 90af08320ea..62d222f9562 100644 --- a/packages/suite/src/actions/bluetooth/bluetoothStartScanningThunk.ts +++ b/packages/suite/src/actions/bluetooth/bluetoothStartScanningThunk.ts @@ -1,11 +1,11 @@ -import { BLUETOOTH_PREFIX, bluetoothScanStatusAction } from '@suite-common/bluetooth'; +import { BLUETOOTH_PREFIX, bluetoothActions } from '@suite-common/bluetooth'; import { createThunk } from '@suite-common/redux-utils'; import { bluetoothIpc } from '@trezor/transport-bluetooth'; export const bluetoothStartScanningThunk = createThunk( `${BLUETOOTH_PREFIX}/bluetoothStartScanningThunk`, (_, { dispatch }) => { - dispatch(bluetoothScanStatusAction({ status: 'running' })); + dispatch(bluetoothActions.scanStatusAction({ status: 'running' })); // This can fail, but if there is an error we already got it from `adapter-event` // and user is informed about it (bluetooth turned-off, ...) bluetoothIpc.startScan(); diff --git a/packages/suite/src/actions/bluetooth/bluetoothStopScanningThunk.ts b/packages/suite/src/actions/bluetooth/bluetoothStopScanningThunk.ts index 445c92db5e9..9599c1d0a2c 100644 --- a/packages/suite/src/actions/bluetooth/bluetoothStopScanningThunk.ts +++ b/packages/suite/src/actions/bluetooth/bluetoothStopScanningThunk.ts @@ -1,11 +1,11 @@ -import { BLUETOOTH_PREFIX, bluetoothScanStatusAction } from '@suite-common/bluetooth'; +import { BLUETOOTH_PREFIX, bluetoothActions } from '@suite-common/bluetooth'; import { createThunk } from '@suite-common/redux-utils'; import { bluetoothIpc } from '@trezor/transport-bluetooth'; export const bluetoothStopScanningThunk = createThunk( `${BLUETOOTH_PREFIX}/bluetoothStopScanningThunk`, (_, { dispatch }) => { - dispatch(bluetoothScanStatusAction({ status: 'idle' })); + dispatch(bluetoothActions.scanStatusAction({ status: 'idle' })); // This can fail, but there is nothing we can do about it bluetoothIpc.stopScan(); }, diff --git a/packages/suite/src/actions/bluetooth/initBluetoothThunk.ts b/packages/suite/src/actions/bluetooth/initBluetoothThunk.ts index ca15dbae90e..f9e16b2bed3 100644 --- a/packages/suite/src/actions/bluetooth/initBluetoothThunk.ts +++ b/packages/suite/src/actions/bluetooth/initBluetoothThunk.ts @@ -1,11 +1,4 @@ -import { - BLUETOOTH_PREFIX, - bluetoothAdapterEventAction, - bluetoothConnectDeviceEventAction, - bluetoothKnownDevicesUpdateAction, - bluetoothNearbyDevicesUpdateAction, - selectKnownDevices, -} from '@suite-common/bluetooth'; +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'; @@ -28,7 +21,7 @@ export const initBluetoothThunk = createThunk( bluetoothIpc.on('adapter-event', isPowered => { console.warn('adapter-event', isPowered); - dispatch(bluetoothAdapterEventAction({ isPowered })); + dispatch(bluetoothActions.adapterEventAction({ isPowered })); }); bluetoothIpc.on('device-list-update', nearbyDevices => { @@ -41,8 +34,10 @@ export const initBluetoothThunk = createThunk( nearbyDevices, }); - dispatch(bluetoothKnownDevicesUpdateAction({ knownDevices: remappedKnownDevices })); - dispatch(bluetoothNearbyDevicesUpdateAction({ nearbyDevices })); + dispatch( + bluetoothActions.knownDevicesUpdateAction({ knownDevices: remappedKnownDevices }), + ); + dispatch(bluetoothActions.nearbyDevicesUpdateAction({ nearbyDevices })); }); bluetoothIpc.on('device-connection-status', connectionStatus => { @@ -53,7 +48,7 @@ export const initBluetoothThunk = createThunk( delete copyConnectionStatus.id; // So we dont pollute redux store dispatch( - bluetoothConnectDeviceEventAction({ + bluetoothActions.connectDeviceEventAction({ id: connectionStatus.id, connectionStatus: copyConnectionStatus, }), diff --git a/packages/suite/src/components/suite/bluetooth/BluetoothConnect.tsx b/packages/suite/src/components/suite/bluetooth/BluetoothConnect.tsx index 85d567312e8..7b35d598df8 100644 --- a/packages/suite/src/components/suite/bluetooth/BluetoothConnect.tsx +++ b/packages/suite/src/components/suite/bluetooth/BluetoothConnect.tsx @@ -1,8 +1,7 @@ import { useCallback, useEffect, useState } from 'react'; import { - bluetoothConnectDeviceEventAction, - bluetoothScanStatusAction, + bluetoothActions, prepareSelectAllDevices, selectAdapterStatus, selectScanStatus, @@ -66,7 +65,7 @@ export const BluetoothConnect = ({ onClose, uiMode }: BluetoothConnectProps) => useEffect(() => { // Intentionally no `clearScamTimer`, this is first run and if we use this we would create infinite re-render const timerId = setTimeout(() => { - dispatch(bluetoothScanStatusAction({ status: 'idle' })); + dispatch(bluetoothActions.scanStatusAction({ status: 'idle' })); }, SCAN_TIMEOUT); setScannerTimerId(timerId); @@ -74,11 +73,11 @@ export const BluetoothConnect = ({ onClose, uiMode }: BluetoothConnectProps) => const onReScanClick = () => { setSelectedDeviceId(null); - dispatch(bluetoothScanStatusAction({ status: 'running' })); + dispatch(bluetoothActions.scanStatusAction({ status: 'running' })); clearScamTimer(); const timerId = setTimeout(() => { - dispatch(bluetoothScanStatusAction({ status: 'idle' })); + dispatch(bluetoothActions.scanStatusAction({ status: 'idle' })); }, SCAN_TIMEOUT); setScannerTimerId(timerId); }; @@ -89,7 +88,7 @@ export const BluetoothConnect = ({ onClose, uiMode }: BluetoothConnectProps) => if (!result.success) { dispatch( - bluetoothConnectDeviceEventAction({ + bluetoothActions.connectDeviceEventAction({ id, connectionStatus: { type: 'error', error: result.error }, }), @@ -102,7 +101,7 @@ export const BluetoothConnect = ({ onClose, uiMode }: BluetoothConnectProps) => ); } else { dispatch( - bluetoothConnectDeviceEventAction({ + bluetoothActions.connectDeviceEventAction({ id, connectionStatus: { type: 'connected' }, }), diff --git a/packages/suite/src/middlewares/wallet/storageMiddleware.ts b/packages/suite/src/middlewares/wallet/storageMiddleware.ts index e554d79f1ad..0c076712804 100644 --- a/packages/suite/src/middlewares/wallet/storageMiddleware.ts +++ b/packages/suite/src/middlewares/wallet/storageMiddleware.ts @@ -2,7 +2,7 @@ import { isAnyOf } from '@reduxjs/toolkit'; import { MiddlewareAPI } from 'redux'; import { analyticsActions } from '@suite-common/analytics'; -import { bluetoothKnownDevicesUpdateAction } from '@suite-common/bluetooth'; +import { bluetoothActions } from '@suite-common/bluetooth'; import { messageSystemActions } from '@suite-common/message-system'; import { isDeviceRemembered } from '@suite-common/suite-utils'; import { TokenManagementAction } from '@suite-common/token-definitions'; @@ -205,7 +205,7 @@ const storageMiddleware = (api: MiddlewareAPI) => { if ( deviceActions.connectDevice.match(action) || - bluetoothKnownDevicesUpdateAction.match(action) + bluetoothActions.knownDevicesUpdateAction.match(action) ) { api.dispatch(storageActions.saveKnownDevices()); } diff --git a/packages/suite/src/types/suite/index.ts b/packages/suite/src/types/suite/index.ts index b63d2dc67e5..e50629c434f 100644 --- a/packages/suite/src/types/suite/index.ts +++ b/packages/suite/src/types/suite/index.ts @@ -2,7 +2,7 @@ import type { Store as ReduxStore } from 'redux'; import type { ThunkAction as TAction, ThunkDispatch } from 'redux-thunk'; import { analyticsActions } from '@suite-common/analytics'; -import { allBluetoothActions } from '@suite-common/bluetooth'; +import { bluetoothActions } from '@suite-common/bluetooth'; import { deviceAuthenticityActions } from '@suite-common/device-authenticity'; import { firmwareActions } from '@suite-common/firmware'; import { messageSystemActions } from '@suite-common/message-system'; @@ -58,7 +58,7 @@ type DiscoveryAction = ReturnType<(typeof discoveryActions)[keyof typeof discove type DeviceAuthenticityAction = ReturnType< (typeof deviceAuthenticityActions)[keyof typeof deviceAuthenticityActions] >; -type BluetoothAction = ReturnType<(typeof allBluetoothActions)[keyof typeof allBluetoothActions]>; +type BluetoothAction = ReturnType<(typeof bluetoothActions)[keyof typeof bluetoothActions]>; // all actions from all apps used to properly type Dispatch. export type Action = diff --git a/packages/suite/src/views/settings/SettingsDevice/BluetoothEraseBonds.tsx b/packages/suite/src/views/settings/SettingsDevice/BluetoothEraseBonds.tsx index 18d31aed008..53b59729a13 100644 --- a/packages/suite/src/views/settings/SettingsDevice/BluetoothEraseBonds.tsx +++ b/packages/suite/src/views/settings/SettingsDevice/BluetoothEraseBonds.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; -import { bluetoothRemoveKnownDeviceAction } from '@suite-common/bluetooth'; +import { bluetoothActions } from '@suite-common/bluetooth'; import { Button } from '@trezor/components'; import TrezorConnect from '@trezor/connect'; @@ -19,7 +19,7 @@ export const BluetoothEraseBonds = () => { const result = await TrezorConnect.eraseBonds({ device }); if (device?.bluetoothProps?.id !== undefined) { - dispatch(bluetoothRemoveKnownDeviceAction({ id: device.bluetoothProps.id })); + dispatch(bluetoothActions.removeKnownDeviceAction({ id: device.bluetoothProps.id })); } console.warn('Erase bonds!', result); From fe0378a017be8c0e0c1e4b46c34b98c554d9f0cb Mon Sep 17 00:00:00 2001 From: Peter Sanderson Date: Fri, 21 Feb 2025 16:16:04 +0100 Subject: [PATCH 12/12] fixup! feat: move reducers into seperate common package, create shared state for mobile --- .../bluetooth/src/bluetoothSelectors.ts | 11 ++--- .../tests/bluetoothSelectors.test.ts | 40 +++++++++---------- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/suite-common/bluetooth/src/bluetoothSelectors.ts b/suite-common/bluetooth/src/bluetoothSelectors.ts index 8e952616ec8..64cd49cad38 100644 --- a/suite-common/bluetooth/src/bluetoothSelectors.ts +++ b/suite-common/bluetooth/src/bluetoothSelectors.ts @@ -26,13 +26,14 @@ export const prepareSelectAllDevices = () => [state => state.bluetooth.nearbyDevices, state => state.bluetooth.knownDevices], (nearbyDevices, knownDevices) => { const map = new Map>(); - knownDevices.forEach(knownDevice => { - map.set(knownDevice.id, { device: knownDevice, status: null }); - }); nearbyDevices.forEach(nearbyDevice => { - if (!map.has(nearbyDevice.device.id)) { - map.set(nearbyDevice.device.id, nearbyDevice); + map.set(nearbyDevice.device.id, nearbyDevice); + }); + + knownDevices.forEach(knownDevice => { + if (!map.has(knownDevice.id)) { + map.set(knownDevice.id, { device: knownDevice, status: null }); } }); diff --git a/suite-common/bluetooth/tests/bluetoothSelectors.test.ts b/suite-common/bluetooth/tests/bluetoothSelectors.test.ts index bb685ae5c34..375dddd75c4 100644 --- a/suite-common/bluetooth/tests/bluetoothSelectors.test.ts +++ b/suite-common/bluetooth/tests/bluetoothSelectors.test.ts @@ -9,38 +9,38 @@ const initialState: BluetoothState = { knownDevices: [] as BluetoothDeviceCommon[], }; +const pairingDeviceStateA: BluetoothDeviceState = { + device: { + id: 'A', + data: [], + name: 'Trezor A', + lastUpdatedTimestamp: 1, + }, + status: { type: 'pairing' }, +}; + +const deviceB: BluetoothDeviceCommon = { + id: 'B', + data: [], + name: 'Trezor B', + lastUpdatedTimestamp: 2, +}; + describe('bluetoothSelectors', () => { it('selects knownDevices and nearbyDevices in one list fot the UI', () => { const selectAllDevices = prepareSelectAllDevices(); - const pairingNearbyDeviceA: BluetoothDeviceState = { - device: { - id: 'A', - data: [], - name: 'Trezor A', - lastUpdatedTimestamp: 1, - }, - status: { type: 'pairing' }, - }; - - const knownDeviceB: BluetoothDeviceCommon = { - id: 'B', - data: [], - name: 'Trezor B', - lastUpdatedTimestamp: 2, - }; - const state: WithBluetoothState = { bluetooth: { ...initialState, - nearbyDevices: [pairingNearbyDeviceA], - knownDevices: [knownDeviceB], + nearbyDevices: [pairingDeviceStateA], + knownDevices: [pairingDeviceStateA.device, deviceB], }, }; const devices = selectAllDevices(state); - expect(devices).toEqual([{ device: knownDeviceB, status: null }, pairingNearbyDeviceA]); + expect(devices).toEqual([{ device: deviceB, status: null }, pairingDeviceStateA]); const devicesSecondTime = selectAllDevices(state); expect(devices === devicesSecondTime).toBe(true); // Asserts that `reselect` memoization works