diff --git a/packages/suite-desktop-ui/src/Main.tsx b/packages/suite-desktop-ui/src/Main.tsx index efbc6504ea73..31c62361d4e9 100644 --- a/packages/suite-desktop-ui/src/Main.tsx +++ b/packages/suite-desktop-ui/src/Main.tsx @@ -30,6 +30,7 @@ import { useDebugLanguageShortcut, useFormattersConfig } from 'src/hooks/suite'; import history from 'src/support/history'; import { ModalContextProvider } from 'src/support/suite/ModalContext'; import { desktopHandshake } from 'src/actions/suite/suiteActions'; +import { initBluetoothThunk } from 'src/actions/bluetooth/initBluetoothThunk'; import * as STORAGE from 'src/actions/suite/constants/storageConstants'; import { DesktopUpdater } from './support/DesktopUpdater'; @@ -134,6 +135,8 @@ export const init = async (container: HTMLElement) => { TrezorConnect[method] = proxy[method]; }); + store.dispatch(initBluetoothThunk()); + // finally render whole app root.render( diff --git a/packages/suite/src/actions/bluetooth/bluetoothActions.ts b/packages/suite/src/actions/bluetooth/bluetoothActions.ts index 16bdc5c83b12..ce9266a98518 100644 --- a/packages/suite/src/actions/bluetooth/bluetoothActions.ts +++ b/packages/suite/src/actions/bluetooth/bluetoothActions.ts @@ -21,16 +21,11 @@ export const bluetoothDeviceListUpdate = createAction( export const bluetoothConnectDeviceEventAction = createAction( `${BLUETOOTH_PREFIX}/device-connection-status`, - ({ connectionStatus }: { connectionStatus: DeviceBluetoothStatus }) => ({ - payload: { connectionStatus }, + ({ connectionStatus, uuid }: { uuid: string; connectionStatus: DeviceBluetoothStatus }) => ({ + payload: { uuid, connectionStatus }, }), ); -export const bluetoothSelectDeviceAction = createAction( - `${BLUETOOTH_PREFIX}/select-device`, - ({ uuid }: { uuid: string | undefined }) => ({ payload: { uuid } }), -); - export const bluetoothScanStatusAction = createAction( `${BLUETOOTH_PREFIX}/scan-status`, ({ status }: { status: BluetoothScanStatus }) => ({ payload: { status } }), @@ -40,6 +35,5 @@ export const allBluetoothActions = { bluetoothAdapterEventAction, bluetoothDeviceListUpdate, bluetoothConnectDeviceEventAction, - bluetoothSelectDeviceAction, bluetoothScanStatusAction, }; diff --git a/packages/suite/src/actions/bluetooth/bluetoothConnectDeviceThunk.ts b/packages/suite/src/actions/bluetooth/bluetoothConnectDeviceThunk.ts new file mode 100644 index 000000000000..3f523c06a5e1 --- /dev/null +++ b/packages/suite/src/actions/bluetooth/bluetoothConnectDeviceThunk.ts @@ -0,0 +1,15 @@ +import { createThunk } from '@suite-common/redux-utils'; +import { bluetoothManager } from '@trezor/transport-bluetooth'; + +import { BLUETOOTH_PREFIX } from './bluetoothActions'; + +type ThunkResponse = ReturnType; + +export const bluetoothConnectDeviceThunk = createThunk( + `${BLUETOOTH_PREFIX}/bluetoothConnectDeviceThunk`, + async ({ uuid }, { fulfillWithValue }) => { + const result = await bluetoothManager.connectDevice(uuid); + + return fulfillWithValue(result); + }, +); diff --git a/packages/suite/src/actions/bluetooth/bluetoothStartScanningThunk.ts b/packages/suite/src/actions/bluetooth/bluetoothStartScanningThunk.ts new file mode 100644 index 000000000000..5b956ef3f622 --- /dev/null +++ b/packages/suite/src/actions/bluetooth/bluetoothStartScanningThunk.ts @@ -0,0 +1,11 @@ +import { createThunk } from '@suite-common/redux-utils'; +import { bluetoothManager } from '@trezor/transport-bluetooth'; + +import { BLUETOOTH_PREFIX } from './bluetoothActions'; + +export const bluetoothStartScanningThunk = createThunk( + `${BLUETOOTH_PREFIX}/bluetoothStartScanningThunk`, + _ => { + bluetoothManager.startScan(); + }, +); diff --git a/packages/suite/src/actions/bluetooth/bluetoothStopScanningThunk.ts b/packages/suite/src/actions/bluetooth/bluetoothStopScanningThunk.ts new file mode 100644 index 000000000000..a98dcb3ff462 --- /dev/null +++ b/packages/suite/src/actions/bluetooth/bluetoothStopScanningThunk.ts @@ -0,0 +1,11 @@ +import { createThunk } from '@suite-common/redux-utils'; +import { bluetoothManager } from '@trezor/transport-bluetooth'; + +import { BLUETOOTH_PREFIX } from './bluetoothActions'; + +export const bluetoothStopScanningThunk = createThunk( + `${BLUETOOTH_PREFIX}/bluetoothStartScanningThunk`, + _ => { + bluetoothManager.stopScan(); + }, +); diff --git a/packages/suite/src/actions/bluetooth/initBluetoothThunk.ts b/packages/suite/src/actions/bluetooth/initBluetoothThunk.ts new file mode 100644 index 000000000000..e553e327ec87 --- /dev/null +++ b/packages/suite/src/actions/bluetooth/initBluetoothThunk.ts @@ -0,0 +1,44 @@ +import { createThunk } from '@suite-common/redux-utils/'; +import { bluetoothManager, DeviceConnectionStatus } from '@trezor/transport-bluetooth'; +import { Without } from '@trezor/type-utils'; + +import { + BLUETOOTH_PREFIX, + bluetoothAdapterEventAction, + bluetoothConnectDeviceEventAction, + bluetoothDeviceListUpdate, +} from './bluetoothActions'; + +type DeviceConnectionStatusWithOptionalUuid = Without & { + uuid?: string; +}; + +export const initBluetoothThunk = createThunk( + `${BLUETOOTH_PREFIX}/initBluetoothThunk`, + (_, { dispatch }) => { + bluetoothManager.on('adapter-event', isPowered => { + console.warn('adapter-event', isPowered); + dispatch(bluetoothAdapterEventAction({ isPowered })); + }); + + bluetoothManager.on('device-list-update', devices => { + console.warn('device-list-update', devices); + dispatch(bluetoothDeviceListUpdate({ devices })); + }); + + bluetoothManager.on('device-connection-status', connectionStatus => { + console.warn('device-connection-status', connectionStatus); + const copyConnectionStatus: DeviceConnectionStatusWithOptionalUuid = { + ...connectionStatus, + }; + delete copyConnectionStatus.uuid; // So we dont pollute redux store + + dispatch( + bluetoothConnectDeviceEventAction({ + uuid: connectionStatus.uuid, + connectionStatus: copyConnectionStatus, + }), + ); + }); + }, +); diff --git a/packages/suite/src/components/suite/bluetooth/BluetoothConnect.tsx b/packages/suite/src/components/suite/bluetooth/BluetoothConnect.tsx index e8d6d47f02c4..8fb7c504f3ce 100644 --- a/packages/suite/src/components/suite/bluetooth/BluetoothConnect.tsx +++ b/packages/suite/src/components/suite/bluetooth/BluetoothConnect.tsx @@ -4,7 +4,6 @@ import TrezorConnect from '@trezor/connect'; import { Card, Column, ElevationUp } from '@trezor/components'; import { spacings } from '@trezor/theme'; import { notificationsActions } from '@suite-common/toast-notifications'; -import { bluetoothManager } from '@trezor/transport-bluetooth'; import { BluetoothNotEnabled } from './errors/BluetoothNotEnabled'; import { BluetoothDeviceList } from './BluetoothDeviceList'; @@ -15,19 +14,18 @@ import { BluetoothScanFooter } from './BluetoothScanFooter'; import { useDispatch, useSelector } from '../../../hooks/suite'; import { BluetoothSelectedDevice } from './BluetoothSelectedDevice'; import { - bluetoothAdapterEventAction, bluetoothConnectDeviceEventAction, - bluetoothDeviceListUpdate, bluetoothScanStatusAction, - bluetoothSelectDeviceAction, } from '../../../actions/bluetooth/bluetoothActions'; import { selectBluetoothDeviceList, selectBluetoothEnabled, selectBluetoothScanStatus, - selectBluetoothSelectedDevice, } from '../../../reducers/bluetooth/bluetoothSelectors'; import { BluetoothPairingPin } from './BluetoothPairingPin'; +import { bluetoothStartScanningThunk } from '../../../actions/bluetooth/bluetoothStartScanningThunk'; +import { bluetoothStopScanningThunk } from '../../../actions/bluetooth/bluetoothStopScanningThunk'; +import { bluetoothConnectDeviceThunk } from '../../../actions/bluetooth/bluetoothConnectDeviceThunk'; const SCAN_TIMEOUT = 30_000; @@ -40,42 +38,22 @@ type TimerId = ReturnType; // Todo: TimerId import type after export const BluetoothConnect = ({ onClose, uiMode }: BluetoothConnectProps) => { const dispatch = useDispatch(); + const [selectedDeviceUuid, setSelectedDeviceUuid] = useState(null); const [scannerTimerId, setScannerTimerId] = useState(null); const isBluetoothEnabled = useSelector(selectBluetoothEnabled); const scanStatus = useSelector(selectBluetoothScanStatus); - const selectedDevice = useSelector(selectBluetoothSelectedDevice); const deviceList = useSelector(selectBluetoothDeviceList); + const devices = Object.values(deviceList); - // Todo: move this to some Singleton component to synchronize the bluetoothManager with Redux State - // Todo: or move to action in the same manner as TrezorConnect.init() is initialized - // See: package/suite/services - useEffect(() => { - bluetoothManager.on('adapter-event', isPowered => { - console.warn('adapter-event', isPowered); - dispatch(bluetoothAdapterEventAction({ isPowered })); - }); - - bluetoothManager.on('device-list-update', devices => { - console.warn('device-list-update', devices); - dispatch(bluetoothDeviceListUpdate({ devices })); - }); - - bluetoothManager.on('device-connection-status', connectionStatus => { - console.warn('device-connection-status', connectionStatus); - dispatch(bluetoothConnectDeviceEventAction({ connectionStatus })); - }); + const selectedDevice = + selectedDeviceUuid !== null ? deviceList[selectedDeviceUuid] ?? null : null; - // Todo: this shall not be on top-level, this shall be called onClick on the connect button - bluetoothManager.startScan(); + useEffect(() => { + dispatch(bluetoothStartScanningThunk()); return () => { - bluetoothManager.removeAllListeners('adapter-event'); - bluetoothManager.removeAllListeners('device-list-update'); - bluetoothManager.removeAllListeners('device-connection-status'); - - // Todo: move this to action and run on close or something - bluetoothManager.stopScan(); + dispatch(bluetoothStopScanningThunk()); }; }, [dispatch]); @@ -95,7 +73,7 @@ export const BluetoothConnect = ({ onClose, uiMode }: BluetoothConnectProps) => }, [dispatch]); const onReScanClick = () => { - dispatch(bluetoothSelectDeviceAction({ uuid: undefined })); + setSelectedDeviceUuid(null); dispatch(bluetoothScanStatusAction({ status: 'running' })); clearScamTimer(); @@ -106,15 +84,15 @@ export const BluetoothConnect = ({ onClose, uiMode }: BluetoothConnectProps) => }; const onSelect = async (uuid: string) => { - dispatch(bluetoothSelectDeviceAction({ uuid })); + setSelectedDeviceUuid(uuid); - // Todo move this to action and call this in thunk - const result = await bluetoothManager.connectDevice(uuid); + const result = await dispatch(bluetoothConnectDeviceThunk({ uuid })).unwrap(); if (!result.success) { dispatch( bluetoothConnectDeviceEventAction({ - connectionStatus: { type: 'error', uuid, error: result.error }, + uuid, + connectionStatus: { type: 'error', error: result.error }, }), ); dispatch( @@ -128,7 +106,8 @@ export const BluetoothConnect = ({ onClose, uiMode }: BluetoothConnectProps) => dispatch( bluetoothConnectDeviceEventAction({ - connectionStatus: { type: 'connected', uuid }, + uuid, + connectionStatus: { type: 'connected' }, }), ); @@ -156,19 +135,19 @@ export const BluetoothConnect = ({ onClose, uiMode }: BluetoothConnectProps) => // This is fake, we scan for devices all the time const isScanning = scanStatus !== 'done'; - const scanFailed = deviceList.length === 0 && scanStatus === 'done'; + const scanFailed = devices.length === 0 && scanStatus === 'done'; const handlePairingCancel = () => { - dispatch(bluetoothSelectDeviceAction({ uuid: undefined })); + setSelectedDeviceUuid(null); onReScanClick(); }; - if (selectedDevice !== undefined && selectedDevice.status.type !== 'pairing') { + if (selectedDevice !== null && selectedDevice.status.type !== 'pairing') { return ; } if ( - selectedDevice !== undefined && + selectedDevice !== null && selectedDevice.status.type === 'pairing' && (selectedDevice.status.pin?.length ?? 0) > 0 ) { @@ -187,7 +166,7 @@ export const BluetoothConnect = ({ onClose, uiMode }: BluetoothConnectProps) => ); @@ -203,7 +182,7 @@ export const BluetoothConnect = ({ onClose, uiMode }: BluetoothConnectProps) => {/* Here we need to do +1 in elevation because of custom design on the welcome screen */} @@ -212,7 +191,7 @@ export const BluetoothConnect = ({ onClose, uiMode }: BluetoothConnectProps) => {uiMode === 'card' && ( )} @@ -225,7 +204,7 @@ export const BluetoothConnect = ({ onClose, uiMode }: BluetoothConnectProps) => diff --git a/packages/suite/src/reducers/bluetooth/bluetoothReducer.ts b/packages/suite/src/reducers/bluetooth/bluetoothReducer.ts index d08e6ebca04e..ad6441f2ad02 100644 --- a/packages/suite/src/reducers/bluetooth/bluetoothReducer.ts +++ b/packages/suite/src/reducers/bluetooth/bluetoothReducer.ts @@ -2,27 +2,29 @@ import { createReducer } from '@reduxjs/toolkit'; import { BluetoothDevice, DeviceConnectionStatus } from '@trezor/transport-bluetooth'; import { deviceActions } from '@suite-common/wallet-core'; +import { Without } from '@trezor/type-utils/'; import { bluetoothAdapterEventAction, bluetoothConnectDeviceEventAction, bluetoothDeviceListUpdate, - bluetoothSelectDeviceAction, bluetoothScanStatusAction, } from '../../actions/bluetooth/bluetoothActions'; +import { bluetoothStartScanningThunk } from '../../actions/bluetooth/bluetoothStartScanningThunk'; +import { bluetoothStopScanningThunk } from '../../actions/bluetooth/bluetoothStopScanningThunk'; export type BluetoothScanStatus = 'running' | 'done'; export type DeviceBluetoothStatus = - | DeviceConnectionStatus - | { - uuid: string; - type: 'found'; - } + | Without // We have UUID in the deviceList map in the state | { type: 'error'; - uuid: string; error: string; + } + | { + // This is state when device is fully connected and dashboard is shown to the user + // At this point we can save the device to the list of paired devices for future reconnects + type: 'connect-connected'; // Todo: Find better naming }; export type DeviceBluetoothStatusType = DeviceBluetoothStatus['type']; @@ -32,13 +34,6 @@ export type BluetoothDeviceState = { status: DeviceBluetoothStatus; }; -// found -// pairing -// paired -// bluetooth-connecting -// bluetooth-connected -// connect-connected - type BluetoothState = { isBluetoothEnabled: boolean; scanStatus: BluetoothScanStatus; @@ -49,15 +44,13 @@ type BluetoothState = { // This list of devices that is union of saved-devices and device that we get from scan deviceList: Record; - - selectedDeviceUuid?: string; // Todo: this shall be stored in the local component }; const initialState: BluetoothState = { isBluetoothEnabled: true, // To prevent the UI from flickering when the page is loaded scanStatus: 'running', // To prevent the UI from flickering when the page is loaded + pairedDevices: [], deviceList: {}, - selectedDeviceUuid: undefined, // Todo: this shall be stored in the local component }; export const bluetoothReducer = createReducer(initialState, builder => @@ -69,58 +62,41 @@ export const bluetoothReducer = createReducer(initialState, builder => } }) .addCase(bluetoothDeviceListUpdate, (state, { payload: { devices } }) => { - devices.forEach(device => { - // Todo: unifiy connectedDevices (saved in DB) and devices, so we drop devices that are no longer visible - - const deviceState = state.deviceList[device.uuid]; - - if (deviceState === undefined) { - state.deviceList[device.uuid] = { + const newList: Record = Object.fromEntries( + state.pairedDevices.map(device => [ + device.uuid, + { device, - status: { type: 'found', uuid: device.uuid }, - }; - } else { - deviceState.device = device; - } - }); - }) - .addCase(bluetoothConnectDeviceEventAction, (state, { payload: { connectionStatus } }) => { - const device = state.deviceList[connectionStatus.uuid]; + status: { type: 'paired' }, + }, + ]), + ); - if (device !== undefined) { - device.status = connectionStatus; - } + devices.forEach(device => { + newList[device.uuid] = { + device, + status: state.deviceList[device.uuid]?.status ?? { type: 'found' }, + }; + }); }) - .addCase(bluetoothSelectDeviceAction, (state, { payload: { uuid } }) => { - if (uuid === undefined) { - state.selectedDeviceUuid = undefined; - - return; - } - - const device = state.deviceList[uuid]; - - state.selectedDeviceUuid = uuid; + .addCase( + bluetoothConnectDeviceEventAction, + (state, { payload: { uuid, connectionStatus } }) => { + const device = state.deviceList[uuid]; - if (device !== undefined) { - device.status = { type: 'pairing', uuid }; // We need to optimistically set the status to pairing so the UI is smooth - } - }) + if (device !== undefined) { + device.status = connectionStatus; + } + }, + ) .addCase(bluetoothScanStatusAction, (state, { payload: { status } }) => { state.scanStatus = status; }) - - // TODO: forgetDevice, save reducer to DB .addCase(deviceActions.deviceDisconnect, (state, { payload: { bluetoothProps } }) => { if (bluetoothProps) { delete state.deviceList[bluetoothProps.uuid]; - if (state.selectedDeviceUuid === bluetoothProps.uuid) { - state.selectedDeviceUuid = undefined; - } } }) - - // TODO: forgetDevice, save reducer to DB .addCase( deviceActions.connectDevice, ( @@ -131,14 +107,17 @@ export const bluetoothReducer = createReducer(initialState, builder => }, }, ) => { - if (bluetoothProps) { - if (state.selectedDeviceUuid === bluetoothProps.uuid) { - state.deviceList[bluetoothProps.uuid].status = { - type: 'connect-connected', - uuid: bluetoothProps.uuid, - }; - } + if (bluetoothProps && bluetoothProps.uuid in state.deviceList) { + state.deviceList[bluetoothProps.uuid].status = { + type: 'connect-connected', + }; } }, - ), + ) + .addCase(bluetoothStartScanningThunk.name, state => { + state.scanStatus = 'running'; + }) + .addCase(bluetoothStopScanningThunk.name, state => { + state.scanStatus = 'done'; + }), ); diff --git a/packages/suite/src/reducers/bluetooth/bluetoothSelectors.ts b/packages/suite/src/reducers/bluetooth/bluetoothSelectors.ts index 0dcb6380322b..97672c10ebf8 100644 --- a/packages/suite/src/reducers/bluetooth/bluetoothSelectors.ts +++ b/packages/suite/src/reducers/bluetooth/bluetoothSelectors.ts @@ -2,12 +2,6 @@ import { AppState } from '../store'; export const selectBluetoothEnabled = (state: AppState) => state.bluetooth.isBluetoothEnabled; -export const selectBluetoothDeviceList = (state: AppState) => - Object.values(state.bluetooth.deviceList); +export const selectBluetoothDeviceList = (state: AppState) => state.bluetooth.deviceList; export const selectBluetoothScanStatus = (state: AppState) => state.bluetooth.scanStatus; - -export const selectBluetoothSelectedDevice = (state: AppState) => - state.bluetooth.selectedDeviceUuid !== undefined - ? state.bluetooth.deviceList[state.bluetooth.selectedDeviceUuid] - : undefined;