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()); }