Skip to content

Commit

Permalink
feat: progress on the UI, implement new structure of the bluetooth re…
Browse files Browse the repository at this point in the history
…dux state and implement connect/disconnect
  • Loading branch information
peter-sanderson committed Feb 27, 2025
1 parent 937dae2 commit 9a68d09
Show file tree
Hide file tree
Showing 12 changed files with 247 additions and 107 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,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 });
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
47 changes: 19 additions & 28 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 @@ -53,28 +48,22 @@ 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;
});

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

useEffect(() => {
Expand Down Expand Up @@ -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') {
Expand All @@ -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 (
<BluetoothPairingPin
device={selectedDevice.device}
pairingPin={selectedDevice.status.pin}
device={selectedDevice}
pairingPin={selectedDevice.connectionStatus.pin}
onCancel={handlePairingCancel}
/>
);
}

if (selectedDevice !== undefined) {
return <BluetoothSelectedDevice device={selectedDevice} onReScanClick={onReScanClick} />;
return (
<BluetoothSelectedDevice
device={selectedDevice}
onReScanClick={onReScanClick}
onCancel={handlePairingCancel}
/>
);
}

const content = scanFailed ? (
Expand All @@ -168,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
Expand Up @@ -36,7 +36,9 @@ export const BluetoothDeviceComponent = ({ device, flex, margin }: BluetoothDevi

<Column justifyContent="start" alignItems="start" flex="1">
<Text typographyStyle="body">Trezor Safe 7</Text>

<Text typographyStyle="hint" variant="tertiary">
<pre>{device.macAddress}</pre>
</Text>
<Row>
<Text typographyStyle="hint" variant="tertiary">
{colorName}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<DeviceBluetoothConnectionStatusType, string> = {
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) => (
<Row onClick={onClick} gap={spacings.md} alignItems="stretch">
<BluetoothDeviceComponent device={device} flex="1" />
<Button
variant="primary"
size="small"
margin={{ vertical: spacings.xxs }}
isDisabled={isDisabled}
>
Connect
</Button>
</Row>
);
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 gap={spacings.md} alignItems="stretch">
<BluetoothDeviceComponent device={device} flex="1" />
<Button
variant="primary"
size="small"
margin={{ vertical: spacings.xxs }}
isDisabled={isDisabled}
isLoading={isLoading || isGlobalLoading}
onClick={handleOnClick}
>
{labelMap[device.connectionStatus.type]}
</Button>
</Row>
);
};
Loading

0 comments on commit 9a68d09

Please sign in to comment.