Skip to content

Commit

Permalink
WIP: design
Browse files Browse the repository at this point in the history
feat: implement Bluetooth Onboarding UI

feat: different 'Not the Trezor you are looking for' UI

feat: implement Bluetooth Onboarding UI 2

feat: implement Bluetooth Onboarding UI

feat: different 'Not the Trezor you are looking for' UI

feat: implement Bluetooth Onboarding UI 2

feat: use CollapsibleBox component in Bluetooth UI

fix: after rebase

enable BT onboarding

WIP: transport name

breaking change

aftr rebase

feat: implement suite-common bluetooth code into Suite
  • Loading branch information
peter-sanderson authored and szymonlesisz committed Feb 25, 2025
1 parent 33919a6 commit bd1861f
Show file tree
Hide file tree
Showing 36 changed files with 1,086 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ const StyledSkeletonRectangle = styled.div<
>`
width: ${({ $width }) => getValue($width) ?? '80px'};
height: ${({ $height }) => getValue($height) ?? '20px'};
background: ${({ $background, ...props }) => $background ?? mapElevationToBackground(props)};
background: ${({ $background, ...props }) =>
$background ??
mapElevationToBackground({
theme: props.theme,
$elevation: props.$elevation,
})};
border-radius: ${({ $borderRadius }) => getValue($borderRadius) ?? borders.radii.xs};
background-size: 200%;
Expand Down
4 changes: 4 additions & 0 deletions packages/suite-desktop-ui/src/Main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,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';
Expand Down Expand Up @@ -136,6 +137,9 @@ export const init = async (container: HTMLElement) => {
TrezorConnect[method] = proxy[method];
});

// init bluetooth module
await store.dispatch(initBluetoothThunk());

// finally render whole app
root.render(
<ReduxProvider store={store}>
Expand Down
1 change: 1 addition & 0 deletions packages/suite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@solana/web3.js": "^2.0.0",
"@suite-common/analytics": "workspace:*",
"@suite-common/assets": "workspace:*",
"@suite-common/bluetooth": "workspace:*",
"@suite-common/connect-init": "workspace:*",
"@suite-common/device-authenticity": "workspace:*",
"@suite-common/fiat-services": "workspace:*",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { BluetoothDevice } from '@trezor/transport-bluetooth';

import { remapKnownDevicesForLinux } from '../remapKnownDevicesForLinux';

const nearbyDeviceA: BluetoothDevice = {
id: 'New-Id-A',
data: [],
name: 'Trezor A',
lastUpdatedTimestamp: 1,
macAddress: 'Address-Trezor-A-Staying-Same',
connected: false,
paired: false,
rssi: 0,
};

const nearbyDeviceC: BluetoothDevice = {
id: 'C',
data: [],
name: 'Trezor C',
lastUpdatedTimestamp: 1,
macAddress: 'Address-Trezor-C',
connected: false,
paired: false,
rssi: 0,
};

const knownDeviceB: BluetoothDevice = {
id: 'B',
data: [],
name: 'Trezor A',
lastUpdatedTimestamp: 1,
macAddress: 'Address-Trezor-B',
connected: false,
paired: false,
rssi: 0,
};

const knownDeviceA: BluetoothDevice = {
id: 'Original-Id A',
data: [],
name: 'Trezor B',
lastUpdatedTimestamp: 2,
macAddress: 'Address-Trezor-A-Staying-Same',
connected: false,
paired: false,
rssi: 0,
};

describe(remapKnownDevicesForLinux.name, () => {
it('remaps the changed id of the device, while leaving the others intact', () => {
const result = remapKnownDevicesForLinux({
nearbyDevices: [nearbyDeviceA, nearbyDeviceC],
knownDevices: [knownDeviceA, knownDeviceB],
});

expect(result).toEqual([
{
address: 'Address-Trezor-A-Staying-Same',
connected: false,
data: [],
id: 'New-Id-A',
lastUpdatedTimestamp: 2,
name: 'Trezor B',
paired: false,
rssi: 0,
},
knownDeviceB, // Is kept as it is
]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { BLUETOOTH_PREFIX, bluetoothActions } 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 BluetoothConnectDeviceThunkResult = {
success: boolean;
};

export const bluetoothConnectDeviceThunk = createThunk<
BluetoothConnectDeviceThunkResult,
{ id: string },
void
>(
`${BLUETOOTH_PREFIX}/bluetoothConnectDeviceThunk`,
async ({ id }, { fulfillWithValue, dispatch }) => {
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 });
},
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { BLUETOOTH_PREFIX, bluetoothActions } from '@suite-common/bluetooth';
import { createThunk } from '@suite-common/redux-utils';
import { bluetoothIpc } from '@trezor/transport-bluetooth';

export const bluetoothStartScanningThunk = createThunk<void, void, void>(
`${BLUETOOTH_PREFIX}/bluetoothStartScanningThunk`,
(_, { dispatch }) => {
dispatch(bluetoothActions.scanStatusAction({ status: 'running' }));
// This can fail, but if there is an error we already got it from `adapter-event`
// and user is informed about it (bluetooth turned-off, ...)
bluetoothIpc.startScan();
},
);
12 changes: 12 additions & 0 deletions packages/suite/src/actions/bluetooth/bluetoothStopScanningThunk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { BLUETOOTH_PREFIX, bluetoothActions } from '@suite-common/bluetooth';
import { createThunk } from '@suite-common/redux-utils';
import { bluetoothIpc } from '@trezor/transport-bluetooth';

export const bluetoothStopScanningThunk = createThunk<void, void, void>(
`${BLUETOOTH_PREFIX}/bluetoothStopScanningThunk`,
(_, { dispatch }) => {
dispatch(bluetoothActions.scanStatusAction({ status: 'idle' }));
// This can fail, but there is nothing we can do about it
bluetoothIpc.stopScan();
},
);
62 changes: 62 additions & 0 deletions packages/suite/src/actions/bluetooth/initBluetoothThunk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { BLUETOOTH_PREFIX, bluetoothActions, selectKnownDevices } from '@suite-common/bluetooth';
import { createThunk } from '@suite-common/redux-utils/';
import { BluetoothDevice, DeviceConnectionStatus, bluetoothIpc } from '@trezor/transport-bluetooth';
import { Without } from '@trezor/type-utils';

import { remapKnownDevicesForLinux } from './remapKnownDevicesForLinux';
import { selectSuiteFlags } from '../../reducers/suite/suiteReducer';

type DeviceConnectionStatusWithOptionalId = Without<DeviceConnectionStatus, 'id'> & {
id?: string;
};

export const initBluetoothThunk = createThunk<void, void, void>(
`${BLUETOOTH_PREFIX}/initBluetoothThunk`,
async (_, { dispatch, getState }) => {
const { isBluetoothEnabled } = selectSuiteFlags(getState());

if (!isBluetoothEnabled) {
return;
}

bluetoothIpc.on('adapter-event', isPowered => {
console.warn('adapter-event', isPowered);
dispatch(bluetoothActions.adapterEventAction({ isPowered }));
});

bluetoothIpc.on('device-list-update', nearbyDevices => {
console.warn('device-list-update', nearbyDevices);

const knownDevices = selectKnownDevices<BluetoothDevice>(getState());

const remappedKnownDevices = remapKnownDevicesForLinux({
knownDevices,
nearbyDevices,
});

dispatch(
bluetoothActions.knownDevicesUpdateAction({ knownDevices: remappedKnownDevices }),
);
dispatch(bluetoothActions.nearbyDevicesUpdateAction({ nearbyDevices }));
});

bluetoothIpc.on('device-connection-status', connectionStatus => {
console.warn('device-connection-status', connectionStatus);
const copyConnectionStatus: DeviceConnectionStatusWithOptionalId = {
...connectionStatus,
};
delete copyConnectionStatus.id; // So we dont pollute redux store

dispatch(
bluetoothActions.connectDeviceEventAction({
id: connectionStatus.id,
connectionStatus: copyConnectionStatus,
}),
);
});

// TODO: this should be called after trezor/connect init?
const knownDevices = selectKnownDevices<BluetoothDevice>(getState());
await bluetoothIpc.init({ knownDevices });
},
);
28 changes: 28 additions & 0 deletions packages/suite/src/actions/bluetooth/remapKnownDevicesForLinux.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { BluetoothDevice } from '@trezor/transport-bluetooth';

type RemapKnownDevicesForLinuxParams = {
knownDevices: BluetoothDevice[];
nearbyDevices: BluetoothDevice[];
};

/**
* On linux, when bluetooth adapter is turned off/on again, the paired
* devices will get different `id`, but `address` will remain the same.
*
* Therefore, we have to remap the knownDevices to change the `id`.
*/
export const remapKnownDevicesForLinux = ({
knownDevices,
nearbyDevices,
}: RemapKnownDevicesForLinuxParams): BluetoothDevice[] =>
knownDevices.map(knownDevice => {
const nearbyDeviceWithSameAddress = nearbyDevices.find(
nearbyDevice =>
nearbyDevice.macAddress === knownDevice.macAddress &&
nearbyDevice.id !== knownDevice.id,
);

return nearbyDeviceWithSameAddress
? { ...knownDevice, id: nearbyDeviceWithSameAddress.id }
: knownDevice;
});
4 changes: 2 additions & 2 deletions packages/suite/src/actions/suite/storageActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ export const saveCoinjoinDebugSettings = () => async (_dispatch: Dispatch, getSt

export const saveKnownDevices = () => async (_dispatch: Dispatch, getState: GetState) => {
if (!(await db.isAccessible())) return;
const { pairedDevices } = getState().bluetooth;
db.addItem('knownDevices', { bluetooth: pairedDevices }, 'devices', true);
const { knownDevices } = getState().bluetooth;
db.addItem('knownDevices', { bluetooth: knownDevices }, 'devices', true);
};

export const saveFormDraft = async (key: string, draft: FieldValues) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { DeviceUpdateRequired } from './DeviceUpdateRequired';
import { DeviceUsedElsewhere } from './DeviceUsedElsewhere';
import { MultiShareBackupInProgress } from './MultiShareBackupInProgress';
import { Transport } from './Transport';
import { BluetoothConnect } from '../bluetooth/BluetoothConnect';

const Wrapper = styled.div`
display: flex;
Expand All @@ -43,11 +44,11 @@ const ButtonWrapper = styled.div`
margin-top: 30px;
`;

const Bluetooth = () => (
const Bluetooth = ({ children }: any) => (
<ElevationContext baseElevation={-1}>
{/* Here we need to draw the inner card with elevation -1 (custom design) */}
<ElevationDown>
<Flex width={470}>Here will be the Bluetooth connection dialog</Flex>
<Flex width={470}>{children}</Flex>
</ElevationDown>
</ElevationContext>
);
Expand Down Expand Up @@ -148,7 +149,12 @@ export const PrerequisitesGuide = ({ allowSwitchDevice }: PrerequisitesGuideProp
return (
<Wrapper>
{isBluetoothConnectOpen ? (
<Bluetooth />
<Bluetooth>
<BluetoothConnect
onClose={() => setIsBluetoothConnectOpen(false)}
uiMode="spatial"
/>
</Bluetooth>
) : (
<NonBluetooth
allowSwitchDevice={allowSwitchDevice}
Expand Down
Loading

0 comments on commit bd1861f

Please sign in to comment.