Skip to content

Commit

Permalink
feat: prepare for usage of connectionStatus from ipc
Browse files Browse the repository at this point in the history
  • Loading branch information
peter-sanderson committed Feb 26, 2025
1 parent c832a6a commit 633b19d
Show file tree
Hide file tree
Showing 18 changed files with 317 additions and 259 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const nearbyDeviceA: BluetoothDevice = {
connected: false,
paired: false,
rssi: 0,
connectionStatus: { type: 'pairing' },
};

const nearbyDeviceC: BluetoothDevice = {
Expand All @@ -22,6 +23,7 @@ const nearbyDeviceC: BluetoothDevice = {
connected: false,
paired: false,
rssi: 0,
connectionStatus: { type: 'pairing' },
};

const knownDeviceB: BluetoothDevice = {
Expand All @@ -33,6 +35,7 @@ const knownDeviceB: BluetoothDevice = {
connected: false,
paired: false,
rssi: 0,
connectionStatus: { type: 'pairing' },
};

const knownDeviceA: BluetoothDevice = {
Expand All @@ -44,6 +47,7 @@ const knownDeviceA: BluetoothDevice = {
connected: false,
paired: false,
rssi: 0,
connectionStatus: { type: 'pairing' },
};

describe(remapKnownDevicesForLinux.name, () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -17,25 +17,12 @@ export const bluetoothConnectDeviceThunk = createThunk<
const result = await bluetoothIpc.connectDevice(id);

if (!result.success) {
dispatch(
bluetoothActions.connectDeviceEventAction({
id,
connectionStatus: { type: 'error', error: result.error },
}),
);
dispatch(
notificationsActions.addToast({
type: 'error',
error: result.error,
}),
);
} else {
dispatch(
bluetoothActions.connectDeviceEventAction({
id,
connectionStatus: { type: 'connected' },
}),
);
}

return fulfillWithValue({ success: result.success });
Expand Down
Original file line number Diff line number Diff line change
@@ -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 });
},
);
9 changes: 2 additions & 7 deletions packages/suite/src/actions/bluetooth/initBluetoothThunk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,10 @@ export const initBluetoothThunk = createThunk<void, void, void>(
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?
Expand Down
1 change: 1 addition & 0 deletions packages/suite/src/actions/suite/storageActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};

Expand Down
37 changes: 10 additions & 27 deletions packages/suite/src/components/suite/bluetooth/BluetoothConnect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand All @@ -43,8 +40,6 @@ export const BluetoothConnect = ({ onClose, uiMode }: BluetoothConnectProps) =>
const [selectedDeviceId, setSelectedDeviceId] = useState<string | null>(null);
const [scannerTimerId, setScannerTimerId] = useState<TimerId | null>(null);

const trezorDevices = useSelector(selectDevices);

const bluetoothAdapterStatus = useSelector(selectAdapterStatus);
const scanStatus = useSelector(selectScanStatus);
const allDevices = useSelector(selectAllDevices);
Expand All @@ -56,27 +51,19 @@ export const BluetoothConnect = ({ onClose, uiMode }: BluetoothConnectProps) =>
console.log('allDevices', allDevices);

const devices = allDevices.filter(it => {
const isDeviceAlreadyConnected =
trezorDevices.find(trezorDevice => trezorDevice.bluetoothProps?.id === it.device.id) !==
undefined;

if (isDeviceAlreadyConnected) {
return false;
}

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

const selectedDevice =
selectedDeviceId !== null
? devices.find(device => device.device.id === selectedDeviceId)
? devices.find(device => device.id === selectedDeviceId)
: undefined;

useEffect(() => {
Expand Down Expand Up @@ -113,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') {
Expand All @@ -145,14 +127,14 @@ 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 (
<BluetoothPairingPin
device={selectedDevice.device}
pairingPin={selectedDevice.status.pin}
device={selectedDevice}
pairingPin={selectedDevice.connectionStatus.pin}
onCancel={handlePairingCancel}
/>
);
Expand All @@ -176,6 +158,7 @@ export const BluetoothConnect = ({ onClose, uiMode }: BluetoothConnectProps) =>
onSelect={onSelect}
deviceList={devices}
isScanning={isScanning}
uiMode={uiMode}
/>
);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,38 +1,87 @@
import { BluetoothDeviceState, DeviceBluetoothStatusType } from '@suite-common/bluetooth';
import { useState } from 'react';

import { DeviceBluetoothConnectionStatusType } from '@suite-common/bluetooth';
import { Button, Row } from '@trezor/components';
import { spacings } from '@trezor/theme';
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 map: Record<DeviceBluetoothStatusType, string> = {
const labelMap: Record<DeviceBluetoothConnectionStatusType, string> = {
disconnected: 'Connect',
connecting: 'Connecting',
connected: 'Connected',
error: 'Error',
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: BluetoothDeviceState<BluetoothDevice>;
onClick: () => void;
device: BluetoothDevice;
onSelect: (id: string) => void;
uiMode: 'spatial' | 'card';
};

export const BluetoothDeviceItem = ({ device, onClick }: BluetoothDeviceItemProps) => {
const isDisabled = device.status !== null;
const isLoading = device.status?.type === 'pairing' || device.status?.type === 'connecting';
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<void>) | 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 (
<Row onClick={/*isDisabled ? undefined : */ onClick} gap={spacings.md} alignItems="stretch">
<BluetoothDeviceComponent device={device.device} flex="1" />
<Row gap={spacings.md} alignItems="stretch">
<BluetoothDeviceComponent device={device} flex="1" />
<Button
variant="primary"
size="small"
margin={{ vertical: spacings.xxs }}
isDisabled={isDisabled}
isLoading={isLoading}
isLoading={isLoading || isGlobalLoading}
onClick={handleOnClick}
>
{device.status ? map[device.status?.type] : 'Connect'}
{labelMap[device.connectionStatus.type]}
</Button>
</Row>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<BluetoothDevice>[];
onSelect: (id: string) => void;
isScanning: boolean;
isDisabled: boolean;
};

const SkeletonDevice = () => (
<Row width="100%" gap={spacings.md} justifyContent="stretch" height="44px" alignItems="center">
<SkeletonRectangle width="44px" height="36px" />
Expand All @@ -23,20 +15,30 @@ const SkeletonDevice = () => (
</Row>
);

type BluetoothDeviceListProps = {
deviceList: BluetoothDevice[];
onSelect: (id: string) => void;
isScanning: boolean;
isDisabled: boolean;
uiMode: 'spatial' | 'card';
};

export const BluetoothDeviceList = ({
onSelect,
deviceList,
isScanning,
uiMode,
}: BluetoothDeviceListProps) => (
<Card>
<Column gap={spacings.md} alignItems="stretch">
{deviceList.map(device => (
<BluetoothDeviceItem
key={device.device.id}
device={device}
onClick={() => onSelect(device.device.id)}
/>
))}
<BluetoothDeviceItem
key={device.id}
device={device}
onSelect={onSelect}
uiMode={uiMode}
/>
))}
{isScanning && <SkeletonDevice />}
</Column>
</Card>
Expand Down
Loading

0 comments on commit 633b19d

Please sign in to comment.