From 01eb8b430c1511d9c1a2de056dfea305a66981ba Mon Sep 17 00:00:00 2001 From: Peter Sanderson Date: Fri, 29 Nov 2024 19:29:17 +0100 Subject: [PATCH] fixup! WIP: design --- .../src/actions/bluetooth/bluetoothActions.ts | 54 ++++++----- .../suite/bluetooth/BluetoothConnect.tsx | 97 ++++++++++--------- .../suite/bluetooth/BluetoothDeviceList.tsx | 10 +- .../suite/bluetooth/BluetoothScanFooter.tsx | 10 +- .../bluetooth/BluetoothSelectedDevice.tsx | 25 +++-- .../reducers/bluetooth/bluetoothReducer.ts | 80 ++++++++++----- .../reducers/bluetooth/bluetoothSelectors.ts | 10 ++ packages/suite/src/types/suite/index.ts | 6 +- 8 files changed, 174 insertions(+), 118 deletions(-) create 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 index 0377c15183c9..1bc15a66646f 100644 --- a/packages/suite/src/actions/bluetooth/bluetoothActions.ts +++ b/packages/suite/src/actions/bluetooth/bluetoothActions.ts @@ -2,42 +2,44 @@ import { createAction } from '@reduxjs/toolkit'; import { ElectronBluetoothDevice } from '@trezor/suite-desktop-api'; +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, - }, - }), + ({ isPowered }: { isPowered: boolean }) => ({ payload: { isPowered } }), ); -export const bluetoothSelectDeviceEventAction = createAction( - `${BLUETOOTH_PREFIX}/select-device-event`, - ({ devices }: { devices: ElectronBluetoothDevice[] }) => ({ - payload: { - devices, - }, - }), +export const bluetoothDeviceListUpdate = createAction( + `${BLUETOOTH_PREFIX}/device-list-update`, + ({ devices }: { devices: ElectronBluetoothDevice[] }) => ({ payload: { devices } }), ); export const bluetoothConnectDeviceEventAction = createAction( - `${BLUETOOTH_PREFIX}/connect-device-event`, - ({ device, phase }: { device: ElectronBluetoothDevice; phase: string }) => ({ - payload: { - device, - phase, - }, + `${BLUETOOTH_PREFIX}/device-connection-status`, + ({ connectionStatus }: { connectionStatus: DeviceBluetoothStatus }) => ({ + payload: { connectionStatus }, }), ); -export const bluetoothPairDeviceEventAction = createAction( - `${BLUETOOTH_PREFIX}/pair-device-event`, - ({ paired, pin }: { paired: boolean; pin: string }) => ({ - payload: { - paired, - pin, - }, - }), +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 } }), +); + +export const allBluetoothActions = { + bluetoothAdapterEventAction, + bluetoothDeviceListUpdate, + bluetoothConnectDeviceEventAction, + bluetoothSelectDeviceAction, + bluetoothScanStatusAction, +}; diff --git a/packages/suite/src/components/suite/bluetooth/BluetoothConnect.tsx b/packages/suite/src/components/suite/bluetooth/BluetoothConnect.tsx index 0eece5ac0a16..4d4f3240d9ac 100644 --- a/packages/suite/src/components/suite/bluetooth/BluetoothConnect.tsx +++ b/packages/suite/src/components/suite/bluetooth/BluetoothConnect.tsx @@ -1,6 +1,6 @@ -import { useEffect, useState } from 'react'; +import { useEffect } from 'react'; -import { desktopApi, ElectronBluetoothDevice } from '@trezor/suite-desktop-api'; +import { desktopApi } from '@trezor/suite-desktop-api'; import TrezorConnect from '@trezor/connect'; import { Card, ElevationUp, Column } from '@trezor/components'; import { spacings } from '@trezor/theme'; @@ -12,51 +12,52 @@ import { BluetoothVersionNotCompatible } from './errors/BluetoothVersionNotCompa import { BluetoothTips } from './BluetoothTips'; import { BluetoothScanHeader } from './BluetoothScanHeader'; import { BluetoothScanFooter } from './BluetoothScanFooter'; -import { useDispatch } from '../../../hooks/suite'; +import { useDispatch, useSelector } from '../../../hooks/suite'; import { BluetoothSelectedDevice } from './BluetoothSelectedDevice'; import { - DeviceBluetoothStatus, - FakeScanStatus, -} from '../../../reducers/bluetooth/bluetoothReducer'; + bluetoothAdapterEventAction, + bluetoothConnectDeviceEventAction, + bluetoothDeviceListUpdate, + bluetoothScanStatusAction, + bluetoothSelectDeviceAction, +} from '../../../actions/bluetooth/bluetoothActions'; +import { + selectBluetoothDeviceList, + selectBluetoothEnabled, + selectBluetoothScanStatus, + selectBluetoothSelectedDevice, +} from '../../../reducers/bluetooth/bluetoothSelectors'; -const FAKE_SCAN_TIMEOUT = 30_000; +const SCAN_TIMEOUT = 30_000; type BluetoothConnectProps = { onClose: () => void; }; export const BluetoothConnect = ({ onClose }: BluetoothConnectProps) => { - const [isBluetoothEnabled, setBluetoothEnabled] = useState(true); // Intentionally by default true so UI wont flicker - const [fakeScanStatus, setFakeScanStatus] = useState('running'); - const [selectedDeviceStatus, setSelectedDeviceStatus] = useState({ - type: 'found', - uuid: 'TODO', - }); - const [deviceList, setDeviceList] = useState([]); - const [selectedDevice, setSelectedDevice] = useState( - undefined, - ); - const dispatch = useDispatch(); + const isBluetoothEnabled = useSelector(selectBluetoothEnabled); + const scanStatus = useSelector(selectBluetoothScanStatus); + const selectedDevice = useSelector(selectBluetoothSelectedDevice); + const deviceList = useSelector(selectBluetoothDeviceList); + + console.log('selectedDevice', selectedDevice); + useEffect(() => { desktopApi.on('bluetooth/adapter-event', isPowered => { console.warn('bluetooth/adapter-event', isPowered); - setBluetoothEnabled(isPowered); - if (!isPowered) { - setDeviceList([]); - } + dispatch(bluetoothAdapterEventAction({ isPowered })); }); - // Todo: rename to something more like: `update-device-list` - desktopApi.on('bluetooth/device-list-update', list => { - console.warn('bluetooth/device-list-update', list); - setDeviceList(list); + desktopApi.on('bluetooth/device-list-update', devices => { + console.warn('bluetooth/device-list-update', devices); + dispatch(bluetoothDeviceListUpdate({ devices })); }); - desktopApi.on('bluetooth/device-connection-status', event => { - console.warn('bluetooth/device-connection-status', event); - setSelectedDeviceStatus(event); + desktopApi.on('bluetooth/device-connection-status', connectionStatus => { + console.warn('bluetooth/device-connection-status', connectionStatus); + dispatch(bluetoothConnectDeviceEventAction({ connectionStatus })); }); desktopApi.bluetoothRequestDevice(); @@ -67,34 +68,37 @@ export const BluetoothConnect = ({ onClose }: BluetoothConnectProps) => { desktopApi.removeAllListeners('bluetooth/device-connection-status'); desktopApi.bluetoothStopScan(); }; - }, []); + }, [dispatch]); useEffect(() => { setTimeout(() => { - setFakeScanStatus('done'); - }, FAKE_SCAN_TIMEOUT); - }, []); + dispatch(bluetoothScanStatusAction({ status: 'done' })); + }, SCAN_TIMEOUT); + }, [dispatch]); const onReScanClick = () => { - setFakeScanStatus('running'); + dispatch(bluetoothScanStatusAction({ status: 'running' })); setTimeout(() => { - setFakeScanStatus('done'); - }, FAKE_SCAN_TIMEOUT); + dispatch(bluetoothScanStatusAction({ status: 'done' })); + }, SCAN_TIMEOUT); }; const onSelect = async (uuid: string) => { console.log('selecting....', uuid); - setSelectedDevice(deviceList.find(d => d.uuid === uuid)); - setSelectedDeviceStatus({ type: 'pairing', uuid }); + dispatch(bluetoothSelectDeviceAction({ uuid })); const result = await desktopApi.bluetoothConnectDevice(uuid); console.log('result', result); if (!result.success) { - setSelectedDeviceStatus({ type: 'error', uuid }); + dispatch( + bluetoothConnectDeviceEventAction({ + connectionStatus: { type: 'error', uuid, error: result.error }, + }), + ); dispatch( notificationsActions.addToast({ type: 'error', @@ -104,7 +108,11 @@ export const BluetoothConnect = ({ onClose }: BluetoothConnectProps) => { } else { // Todo: What to do with error in this flow? UI-Wise - setSelectedDeviceStatus({ type: 'connected', uuid }); + dispatch( + bluetoothConnectDeviceEventAction({ + connectionStatus: { type: 'connected', uuid }, + }), + ); // WAIT for connect event, TODO: figure out better way const closePopupAfterConnection = () => { @@ -127,15 +135,14 @@ export const BluetoothConnect = ({ onClose }: BluetoothConnectProps) => { } // This is fake, we scan for devices all the time - const isScanning = fakeScanStatus !== 'done'; - const scanFailed = deviceList.length === 0 && fakeScanStatus === 'done'; + const isScanning = scanStatus !== 'done'; + const scanFailed = deviceList.length === 0 && scanStatus === 'done'; if (selectedDevice !== undefined) { return ( setSelectedDevice(undefined)} + onCancel={() => dispatch(bluetoothSelectDeviceAction({ uuid: undefined }))} /> ); } @@ -171,7 +178,7 @@ export const BluetoothConnect = ({ onClose }: BluetoothConnectProps) => { ); diff --git a/packages/suite/src/components/suite/bluetooth/BluetoothDeviceList.tsx b/packages/suite/src/components/suite/bluetooth/BluetoothDeviceList.tsx index e486ed8618c0..84d9a7d6bbd0 100644 --- a/packages/suite/src/components/suite/bluetooth/BluetoothDeviceList.tsx +++ b/packages/suite/src/components/suite/bluetooth/BluetoothDeviceList.tsx @@ -1,11 +1,11 @@ import { Card, Column, SkeletonRectangle, Row } from '@trezor/components'; import { spacings } from '@trezor/theme'; -import { ElectronBluetoothDevice } from '@trezor/suite-desktop-api'; import { BluetoothDeviceItem } from './BluetoothDeviceItem'; +import { BluetoothDeviceState } from '../../../reducers/bluetooth/bluetoothReducer'; type BluetoothDeviceListProps = { - deviceList: ElectronBluetoothDevice[]; + deviceList: BluetoothDeviceState[]; onSelect: (uuid: string) => void; isScanning: boolean; }; @@ -31,9 +31,9 @@ export const BluetoothDeviceList = ({ {deviceList.map(d => ( onSelect(d.uuid)} + key={d.device.uuid} + device={d.device} + onClick={() => onSelect(d.device.uuid)} connecting={false} lastSeenTimestamp={0} /> diff --git a/packages/suite/src/components/suite/bluetooth/BluetoothScanFooter.tsx b/packages/suite/src/components/suite/bluetooth/BluetoothScanFooter.tsx index a0739e89712f..5e333d0938b1 100644 --- a/packages/suite/src/components/suite/bluetooth/BluetoothScanFooter.tsx +++ b/packages/suite/src/components/suite/bluetooth/BluetoothScanFooter.tsx @@ -2,20 +2,20 @@ import { Text } from '@trezor/components'; import { spacings } from '@trezor/theme'; import { NotTrezorYouAreLookingFor } from './NotTrezorYouAreLookingFor'; -import { FakeScanStatus } from './types'; +import { BluetoothScanStatus } from '../../../reducers/bluetooth/bluetoothReducer'; type BluetoothScanFooterProps = { onReScanClick: () => void; - fakeScanStatus: FakeScanStatus; + scanStatus: BluetoothScanStatus; numberOfDevices: number; }; export const BluetoothScanFooter = ({ onReScanClick, - fakeScanStatus, + scanStatus, numberOfDevices, }: BluetoothScanFooterProps) => { - if (fakeScanStatus === 'running') { + if (scanStatus === 'running') { return ( Make sure your TS7 is on and in pairing mode (hold power button) @@ -23,7 +23,7 @@ export const BluetoothScanFooter = ({ ); } - if (fakeScanStatus === 'done' && numberOfDevices > 0) { + if (scanStatus === 'done' && numberOfDevices > 0) { return ; } diff --git a/packages/suite/src/components/suite/bluetooth/BluetoothSelectedDevice.tsx b/packages/suite/src/components/suite/bluetooth/BluetoothSelectedDevice.tsx index 001bf7f3ada4..85e1b0af7ddb 100644 --- a/packages/suite/src/components/suite/bluetooth/BluetoothSelectedDevice.tsx +++ b/packages/suite/src/components/suite/bluetooth/BluetoothSelectedDevice.tsx @@ -1,16 +1,17 @@ import { ReactNode } from 'react'; -import { ElectronBluetoothDevice } from '@trezor/suite-desktop-api'; import { Card, ElevationContext, Icon, Row, Spinner, Text } from '@trezor/components'; import { spacings } from '@trezor/theme'; import { BluetoothDevice } from './BluetoothDevice'; -import { DeviceBluetoothStatus, DeviceBluetoothStatusType } from './types'; import { BluetoothPairingPin } from './BluetoothPairingPin'; +import { + BluetoothDeviceState, + DeviceBluetoothStatusType, +} from '../../../reducers/bluetooth/bluetoothReducer'; export type BluetoothSelectedDeviceProps = { - device: ElectronBluetoothDevice; - status: DeviceBluetoothStatus; + device: BluetoothDeviceState; onCancel: () => void; }; @@ -21,11 +22,7 @@ const PairingComponent = () => ( ); -export const BluetoothSelectedDevice = ({ - device, - status, - onCancel, -}: BluetoothSelectedDeviceProps) => { +export const BluetoothSelectedDevice = ({ device, onCancel }: BluetoothSelectedDeviceProps) => { const map: Record ReactNode) | null> = { found: null, error: () => ( @@ -47,11 +44,11 @@ export const BluetoothSelectedDevice = ({ ), }; - if (status.type === 'pairing' && (status.pin?.length ?? 0) > 0) { + if (device.status.type === 'pairing' && (device.status.pin?.length ?? 0) > 0) { return ( {}} /> @@ -62,8 +59,8 @@ export const BluetoothSelectedDevice = ({ - - {map[status.type]?.()} + + {map[device.status.type]?.()} diff --git a/packages/suite/src/reducers/bluetooth/bluetoothReducer.ts b/packages/suite/src/reducers/bluetooth/bluetoothReducer.ts index b17222618386..b956c3b6a99a 100644 --- a/packages/suite/src/reducers/bluetooth/bluetoothReducer.ts +++ b/packages/suite/src/reducers/bluetooth/bluetoothReducer.ts @@ -8,51 +8,87 @@ import { import { bluetoothAdapterEventAction, bluetoothConnectDeviceEventAction, - bluetoothPairDeviceEventAction, - bluetoothSelectDeviceEventAction, + bluetoothDeviceListUpdate, + bluetoothSelectDeviceAction, } from '../../actions/bluetooth/bluetoothActions'; -export type FakeScanStatus = 'running' | 'done'; +export type BluetoothScanStatus = 'running' | 'done'; export type DeviceBluetoothStatus = | BluetoothDeviceConnectionStatus | { uuid: string; type: 'found' | 'error'; + } + | { + uuid: string; + type: 'error'; + error: string; }; export type DeviceBluetoothStatusType = DeviceBluetoothStatus['type']; -// Todo: discuss with Native Guys +export type BluetoothDeviceState = { + device: ElectronBluetoothDevice; + status: DeviceBluetoothStatus; +}; type BluetoothState = { isBluetoothEnabled: boolean; - fakeScanStatus: FakeScanStatus; - deviceList: { device: ElectronBluetoothDevice; pairingStatus: DeviceBluetoothStatus }[]; - - selectedDevice?: string; + scanStatus: BluetoothScanStatus; + deviceList: Record; + selectedDeviceUuid?: string; }; const initialState: BluetoothState = { - isBluetoothEnabled: false, - fakeScanStatus: 'done', - deviceList: [], - selectedDevice: undefined, + isBluetoothEnabled: true, // To prevent the UI from flickering when the page is loaded + scanStatus: 'done', + deviceList: {}, + selectedDeviceUuid: undefined, }; -export const bluetoothReducer = createReducer(initialState, builder => { +export const bluetoothReducer = createReducer(initialState, builder => builder .addCase(bluetoothAdapterEventAction, (state, { payload: { isPowered } }) => { state.isBluetoothEnabled = isPowered; + if (!isPowered) { + state.deviceList = {}; + } }) - .addCase(bluetoothSelectDeviceEventAction, (state, { payload: { devices } }) => { - state.deviceList = devices; + .addCase(bluetoothDeviceListUpdate, (state, { payload: { devices } }) => { + devices.forEach(device => { + const deviceState = state.deviceList[device.uuid]; + + if (deviceState === undefined) { + state.deviceList[device.uuid] = { + device, + status: { type: 'found', uuid: device.uuid }, + }; + } else { + deviceState.device = device; + } + }); }) - .addCase(bluetoothConnectDeviceEventAction, (state, { payload: { device, phase } }) => { - // state.selectedDevice; - // Todo: solve + .addCase(bluetoothConnectDeviceEventAction, (state, { payload: { connectionStatus } }) => { + const device = state.deviceList[connectionStatus.uuid]; + + if (device !== undefined) { + device.status = connectionStatus; + } }) - .addCase(bluetoothPairDeviceEventAction, (state, { payload: { paired, pin } }) => { - state.selectedDeviceStatus = { type: paired ? 'paired' : 'pairing', pin }; - }); -}); + .addCase(bluetoothSelectDeviceAction, (state, { payload: { uuid } }) => { + if (uuid === undefined) { + state.selectedDeviceUuid = undefined; + + return; + } + + const device = state.deviceList[uuid]; + + state.selectedDeviceUuid = uuid; + + if (device !== undefined) { + device.status = { type: 'connecting', uuid }; + } + }), +); diff --git a/packages/suite/src/reducers/bluetooth/bluetoothSelectors.ts b/packages/suite/src/reducers/bluetooth/bluetoothSelectors.ts new file mode 100644 index 000000000000..b31e4cc26b21 --- /dev/null +++ b/packages/suite/src/reducers/bluetooth/bluetoothSelectors.ts @@ -0,0 +1,10 @@ +import { AppState } from '../store'; + +export const selectBluetoothEnabled = (state: AppState) => state.bluetooth.isBluetoothEnabled; +export const selectBluetoothDeviceList = (state: AppState) => + Object.values(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; diff --git a/packages/suite/src/types/suite/index.ts b/packages/suite/src/types/suite/index.ts index 9a6dc87b6351..6ca988fc9294 100644 --- a/packages/suite/src/types/suite/index.ts +++ b/packages/suite/src/types/suite/index.ts @@ -29,6 +29,7 @@ import type { WalletAction } from 'src/types/wallet'; import type { BackupAction } from 'src/actions/backup/backupActions'; import type { RecoveryAction } from 'src/actions/recovery/recoveryActions'; import type { GuideAction } from 'src/actions/suite/guideActions'; +import { allBluetoothActions } from 'src/actions/bluetooth/bluetoothActions'; // reexport export type { ExtendedMessageDescriptor } from 'src/components/suite/Translation'; @@ -62,6 +63,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]>; // all actions from all apps used to properly type Dispatch. export type Action = @@ -87,7 +89,8 @@ export type Action = | ProtocolAction | DiscoveryAction | DeviceAction - | DeviceAuthenticityAction; + | DeviceAuthenticityAction + | BluetoothAction; export type ThunkAction = TAction; @@ -115,6 +118,7 @@ export type ForegroundAppProps = { export type ToastNotificationVariant = 'success' | 'info' | 'warning' | 'error' | 'transparent'; export { TorStatus } from '@trezor/suite-desktop-api/src/enums'; + export interface TorBootstrap { current: number; total: number;