diff --git a/packages/components/src/components/skeletons/SkeletonRectangle.tsx b/packages/components/src/components/skeletons/SkeletonRectangle.tsx
index b9b8d0f82a49..51b1245e6a15 100644
--- a/packages/components/src/components/skeletons/SkeletonRectangle.tsx
+++ b/packages/components/src/components/skeletons/SkeletonRectangle.tsx
@@ -1,6 +1,6 @@
import styled, { css } from 'styled-components';
-import { Elevation, borders, mapElevationToBackground } from '@trezor/theme';
+import { Elevation, borders, mapElevationToBackground, nextElevation } from '@trezor/theme';
import { SkeletonBaseProps } from './types';
import { getValue, shimmerEffect } from './utils';
@@ -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%;
diff --git a/packages/product-components/src/components/RotateDeviceImage/RotateDeviceImage.tsx b/packages/product-components/src/components/RotateDeviceImage/RotateDeviceImage.tsx
index 7c6407a3dc0b..f9673325e1ef 100644
--- a/packages/product-components/src/components/RotateDeviceImage/RotateDeviceImage.tsx
+++ b/packages/product-components/src/components/RotateDeviceImage/RotateDeviceImage.tsx
@@ -32,9 +32,12 @@ export const RotateDeviceImage = ({
const isDeviceImageRotating =
deviceModel &&
- [DeviceModelInternal.T2B1, DeviceModelInternal.T3B1, DeviceModelInternal.T3T1].includes(
- deviceModel,
- );
+ [
+ DeviceModelInternal.T2B1,
+ DeviceModelInternal.T3B1,
+ DeviceModelInternal.T3T1,
+ DeviceModelInternal.T3W1,
+ ].includes(deviceModel);
return (
<>
diff --git a/packages/suite-desktop-core/src/modules/bluetooth.ts b/packages/suite-desktop-core/src/modules/bluetooth.ts
index 1b9305ef60ee..205f83058684 100644
--- a/packages/suite-desktop-core/src/modules/bluetooth.ts
+++ b/packages/suite-desktop-core/src/modules/bluetooth.ts
@@ -63,7 +63,12 @@ export const init: ModuleInit = ({ mainWindowProxy }) => {
ipcMain.handle('bluetooth/connect-device', async (_, uuid) => {
const api = new TrezorBle({});
- await api.connect();
+ const connectionError = await api
+ .connect()
+ .catch(error => ({ success: false, error: error.message }));
+ if (connectionError) {
+ return connectionError;
+ }
const emitStatus = (event: BluetoothDeviceConnectionStatus) => {
mainWindowProxy
@@ -89,37 +94,39 @@ export const init: ModuleInit = ({ mainWindowProxy }) => {
}
});
- const result = await api.sendMessage('connect_device', uuid);
+ const result = await api
+ .sendMessage('connect_device', uuid)
+ .then(() => ({ success: true }) as const)
+ .catch(error => ({ success: false, error: error.message }));
+
console.warn('Connect result', result);
api.disconnect();
- if (result !== true) {
- console.warn('ERROR!', result);
- // mainWindowProxy
- // .getInstance()
- // ?.webContents.send('bluetooth/connect-device-event', { phase: 'error' });
-
- return { success: false, error: result };
- } else {
- return { success: true };
- }
+ return result;
});
ipcMain.handle('bluetooth/forget-device', async (_, uuid) => {
const api = new TrezorBle({});
- await api.connect();
+ const connectionError = await api
+ .connect()
+ .catch(error => ({ success: false, error: error.message }));
+ if (connectionError) {
+ return connectionError;
+ }
- const result = await api.sendMessage('forget_device', uuid);
+ const result = await api
+ .sendMessage('forget_device', uuid)
+ .then(() => ({ success: true }) as const)
+ .catch(error => ({ success: false, error: error.message }));
console.warn('Forget result', result);
api.disconnect();
- return { success: true };
+ return result;
});
ipcMain.on('bluetooth/request-device', async () => {
- console.warn('CALLIN BT REQUEST DEVICE!');
const api = new TrezorBle({});
const emitSelect = ({ devices }: { devices: BluetoothDevice[] }) => {
@@ -134,16 +141,29 @@ export const init: ModuleInit = ({ mainWindowProxy }) => {
if (!powered) {
// api.sendMessage('stop_scan');
} else {
- api.sendMessage('start_scan');
+ api.sendMessage('start_scan').catch(error => {
+ console.warn('Start scan error', error);
+ });
}
};
emitSelect({ devices: [] });
- // TODO: here race condition with bluetooth/select-device response?
- await api.connect();
- // const info = await api.sendMessage('get_info', true);
- const info = await api.sendMessage('get_info');
+ const connectionError = await api
+ .connect()
+ .catch(error => ({ success: false, error: error.message }));
+ if (connectionError) {
+ return connectionError;
+ }
+
+ const info = await api
+ .sendMessage('get_info')
+ .catch(error => ({ success: false, error: error.message }));
+ if ('success' in info) {
+ console.warn('Api info error', info);
+
+ return info;
+ }
console.warn('Api info', info);
@@ -157,40 +177,16 @@ export const init: ModuleInit = ({ mainWindowProxy }) => {
api.on('DeviceDisconnected', emitSelect);
api.on('AdapterStateChanged', emitAdapterState);
- const devices = await api.sendMessage('start_scan');
-
- emitSelect({ devices });
-
- // const clear = () => {
- // ipcMain.removeAllListeners('bluetooth/stop-scan');
- // // ipcMain.removeHandler('bluetooth/connect-device');
- // api.removeAllListeners();
- // // await api.sendMessage('stop_scan');
- // api.disconnect();
- // };
-
- // const handler = async (_: any, deviceId: string) => {
- // logger.info(SERVICE_NAME, 'Received device ' + deviceId);
- // console.warn('bluetooth/connect-device', deviceId);
- // if (!deviceId) {
- // clear();
-
- // return { success: true };
- // }
- // const connected = await api.sendMessage('connect_device', deviceId);
- // if (connected !== true) {
- // console.warn('ERROR!', connected);
- // // mainWindowProxy
- // // .getInstance()
- // // ?.webContents.send('bluetooth/connect-device-event', { phase: 'error' });
-
- // return { success: false, error: connected };
- // } else {
- // clear();
- // }
-
- // return { success: true };
- // };
+ const result = await api
+ .sendMessage('start_scan')
+ .catch(error => ({ success: false, error: error.message }));
+ if ('success' in result) {
+ console.warn('Start scan error', result);
+
+ return result;
+ }
+
+ emitSelect({ devices: result });
ipcMain.on('bluetooth/stop-scan', () => {
ipcMain.removeAllListeners('bluetooth/stop-scan');
diff --git a/packages/suite/src/components/suite/PrerequisitesGuide/DeviceConnect.tsx b/packages/suite/src/components/suite/PrerequisitesGuide/DeviceConnect.tsx
index 4707675ac4ad..5008d0953e13 100644
--- a/packages/suite/src/components/suite/PrerequisitesGuide/DeviceConnect.tsx
+++ b/packages/suite/src/components/suite/PrerequisitesGuide/DeviceConnect.tsx
@@ -1,3 +1,5 @@
+import { Button } from '@trezor/components';
+
import { Translation, TroubleshootingTips, WebUsbButton } from 'src/components/suite';
import {
TROUBLESHOOTING_TIP_BRIDGE_STATUS,
@@ -10,9 +12,15 @@ import {
interface DeviceConnectProps {
isWebUsbTransport: boolean;
+ isBluetooth: boolean;
+ onBluetoothClick: () => void;
}
-export const DeviceConnect = ({ isWebUsbTransport }: DeviceConnectProps) => {
+export const DeviceConnect = ({
+ isWebUsbTransport,
+ onBluetoothClick,
+ isBluetooth,
+}: DeviceConnectProps) => {
const items = isWebUsbTransport
? [
TROUBLESHOOTING_TIP_UDEV,
@@ -32,7 +40,23 @@ export const DeviceConnect = ({ isWebUsbTransport }: DeviceConnectProps) => {
}
items={items}
- cta={isWebUsbTransport ? : undefined}
+ cta={
+ // eslint-disable-next-line no-nested-ternary
+ isBluetooth ? (
+
+ ) : isWebUsbTransport ? (
+
+ ) : undefined
+ }
data-testid="@connect-device-prompt/no-device-detected"
/>
);
diff --git a/packages/suite/src/components/suite/PrerequisitesGuide/PrerequisitesGuide.tsx b/packages/suite/src/components/suite/PrerequisitesGuide/PrerequisitesGuide.tsx
index 93e33acedf26..8fa3d6aaf893 100644
--- a/packages/suite/src/components/suite/PrerequisitesGuide/PrerequisitesGuide.tsx
+++ b/packages/suite/src/components/suite/PrerequisitesGuide/PrerequisitesGuide.tsx
@@ -1,14 +1,14 @@
-import { useMemo } from 'react';
+import { useMemo, useState } from 'react';
import styled from 'styled-components';
import { motion } from 'framer-motion';
import { getStatus, deviceNeedsAttention } from '@suite-common/suite-utils';
-import { Button, motionEasing } from '@trezor/components';
+import { Button, ElevationContext, ElevationDown, motionEasing } from '@trezor/components';
import { selectDevices, selectDevice } from '@suite-common/wallet-core';
import { ConnectDevicePrompt, Translation } from 'src/components/suite';
-import { isWebUsb } from 'src/utils/suite/transport';
+import { isBluetoothTransport, isWebUsb } from 'src/utils/suite/transport';
import { useDispatch, useSelector } from 'src/hooks/suite';
import { selectPrerequisite } from 'src/reducers/suite/suiteReducer';
import { goto } from 'src/actions/suite/routerActions';
@@ -26,6 +26,7 @@ import { DeviceNoFirmware } from './DeviceNoFirmware';
import { DeviceUpdateRequired } from './DeviceUpdateRequired';
import { DeviceDisconnectRequired } from './DeviceDisconnectRequired';
import { MultiShareBackupInProgress } from './MultiShareBackupInProgress';
+import { BluetoothConnect } from '../bluetooth/BluetoothConnect';
const Wrapper = styled.div`
display: flex;
@@ -48,7 +49,10 @@ interface PrerequisitesGuideProps {
}
export const PrerequisitesGuide = ({ allowSwitchDevice }: PrerequisitesGuideProps) => {
+ const [isBluetoothConnectOpen, setIsBluetoothConnectOpen] = useState(false);
+
const dispatch = useDispatch();
+
const device = useSelector(selectDevice);
const devices = useSelector(selectDevices);
const connectedDevicesCount = devices.filter(d => d.connected === true).length;
@@ -56,6 +60,7 @@ export const PrerequisitesGuide = ({ allowSwitchDevice }: PrerequisitesGuideProp
const prerequisite = useSelector(selectPrerequisite);
const isWebUsbTransport = isWebUsb(transport);
+ const isBluetooth = isBluetoothTransport(transport);
const TipComponent = useMemo(
() => (): React.JSX.Element => {
@@ -65,7 +70,13 @@ export const PrerequisitesGuide = ({ allowSwitchDevice }: PrerequisitesGuideProp
case 'device-disconnect-required':
return ;
case 'device-disconnected':
- return ;
+ return (
+ setIsBluetoothConnectOpen(true)}
+ />
+ );
case 'device-unacquired':
return ;
case 'device-unreadable':
@@ -91,7 +102,7 @@ export const PrerequisitesGuide = ({ allowSwitchDevice }: PrerequisitesGuideProp
return <>>;
}
},
- [prerequisite, isWebUsbTransport, device],
+ [prerequisite, isWebUsbTransport, isBluetooth, device],
);
const handleSwitchDeviceClick = () =>
@@ -99,30 +110,41 @@ export const PrerequisitesGuide = ({ allowSwitchDevice }: PrerequisitesGuideProp
return (
-
-
- {allowSwitchDevice && connectedDevicesCount > 1 && (
-
-
-
+ {isBluetoothConnectOpen ? (
+
+ {/* Here we need to draw the inner card with elevation -1 (custom design) */}
+
+ setIsBluetoothConnectOpen(false)} />
+
+
+ ) : (
+ <>
+
+
+ {allowSwitchDevice && connectedDevicesCount > 1 && (
+
+
+
+ )}
+
+
+
+
+ >
)}
-
-
-
-
);
};
diff --git a/packages/suite/src/components/suite/bluetooth/BluetoothConnect.tsx b/packages/suite/src/components/suite/bluetooth/BluetoothConnect.tsx
new file mode 100644
index 000000000000..fc6c2f37ff5e
--- /dev/null
+++ b/packages/suite/src/components/suite/bluetooth/BluetoothConnect.tsx
@@ -0,0 +1,177 @@
+import { useEffect, useState } from 'react';
+
+import { desktopApi, ElectronBluetoothDevice } from '@trezor/suite-desktop-api';
+import TrezorConnect from '@trezor/connect';
+import { Card, ElevationUp, Column } from '@trezor/components';
+import { spacings } from '@trezor/theme';
+import { notificationsActions } from '@suite-common/toast-notifications';
+
+import { BluetoothNotEnabled } from './errors/BluetoothNotEnabled';
+import { BluetoothDeviceList } from './BluetoothDeviceList';
+import { BluetoothVersionNotCompatible } from './errors/BluetoothVersionNotCompatible';
+import { BluetoothTips } from './BluetoothTips';
+import { BluetoothPairingPin } from './BluetoothPairingPin';
+import { BluetoothScanHeader } from './BluetoothScanHeader';
+import { BluetoothScanFooter } from './BluetoothScanFooter';
+import { FakeScanStatus, DeviceBluetoothStatus } from './types';
+import { useDispatch } from '../../../hooks/suite';
+import { BluetoothSelectedDevice } from './BluetoothSelectedDevice';
+
+const FAKE_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();
+
+ useEffect(() => {
+ desktopApi.on('bluetooth/adapter-event', isPowered => {
+ console.warn('bluetooth/adapter-event', isPowered);
+ setBluetoothEnabled(isPowered);
+ if (!isPowered) {
+ setDeviceList([]);
+ }
+ });
+
+ desktopApi.on('bluetooth/device-list-update', list => {
+ console.warn('bluetooth/device-list-update', list);
+ setDeviceList(list);
+ });
+
+ desktopApi.on('bluetooth/device-connection-status', event => {
+ console.warn('bluetooth/device-connection-status', event);
+ setSelectedDeviceStatus(event);
+ });
+
+ desktopApi.bluetoothRequestDevice();
+
+ return () => {
+ desktopApi.removeAllListeners('bluetooth/adapter-event');
+ desktopApi.removeAllListeners('bluetooth/device-list-update');
+ desktopApi.removeAllListeners('bluetooth/device-connection-status');
+ desktopApi.bluetoothStopScan();
+ };
+ }, []);
+
+ useEffect(() => {
+ setTimeout(() => {
+ setFakeScanStatus('done');
+ }, FAKE_SCAN_TIMEOUT);
+ }, []);
+
+ const onReScanClick = () => {
+ setFakeScanStatus('running');
+
+ setTimeout(() => {
+ setFakeScanStatus('done');
+ }, FAKE_SCAN_TIMEOUT);
+ };
+
+ const onSelect = async (uuid: string) => {
+ console.log('selecting....', uuid);
+
+ setSelectedDevice(deviceList.find(d => d.uuid === uuid));
+ setSelectedDeviceStatus({ type: 'pairing', uuid });
+
+ const result = await desktopApi.bluetoothConnectDevice(uuid);
+
+ console.log('result', result);
+
+ if (!result.success) {
+ setSelectedDeviceStatus({ type: 'error', uuid });
+ dispatch(
+ notificationsActions.addToast({
+ type: 'error',
+ error: result.error,
+ }),
+ );
+ } else {
+ // Todo: What to do with error in this flow? UI-Wise
+
+ setSelectedDeviceStatus({ type: 'connected', uuid });
+
+ // WAIT for connect event, TODO: figure out better way
+ const closePopupAfterConnection = () => {
+ TrezorConnect.off('device-connect', closePopupAfterConnection);
+ TrezorConnect.off('device-connect_unacquired', closePopupAfterConnection);
+ // setSelectedDeviceStatus({ type: 'error', uuid }); // Todo: what here?
+ };
+ TrezorConnect.on('device-connect', closePopupAfterConnection);
+ TrezorConnect.on('device-connect_unacquired', closePopupAfterConnection);
+ }
+ };
+
+ // const isLoading = connectingStatus && connectingStatus.status !== 'error';
+
+ if (!isBluetoothEnabled) {
+ return ;
+ }
+
+ // Todo: incompatible version
+ if (false) {
+ return ;
+ }
+
+ // This is fake, we scan for devices all the time
+ const isScanning = fakeScanStatus !== 'done';
+ const scanFailed = deviceList.length === 0 && fakeScanStatus === 'done';
+
+ if (selectedDevice !== undefined) {
+ return (
+ setSelectedDevice(undefined)}
+ />
+ );
+ }
+
+ return (
+
+
+
+
+
+
+ {scanFailed ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+ );
+};
diff --git a/packages/suite/src/components/suite/bluetooth/BluetoothDevice.tsx b/packages/suite/src/components/suite/bluetooth/BluetoothDevice.tsx
new file mode 100644
index 000000000000..12ae4a0f8074
--- /dev/null
+++ b/packages/suite/src/components/suite/bluetooth/BluetoothDevice.tsx
@@ -0,0 +1,54 @@
+import { Column, FlexProps, Icon, Row, Text } from '@trezor/components';
+import { RotateDeviceImage } from '@trezor/product-components';
+import { spacings } from '@trezor/theme';
+import { ElectronBluetoothDevice } from '@trezor/suite-desktop-api';
+import { models } from '@trezor/connect/src/data/models'; // Todo: solve this import issue
+import { DeviceModelInternal } from '@trezor/connect';
+
+type BluetoothDeviceProps = {
+ device: ElectronBluetoothDevice;
+ flex?: FlexProps['flex'];
+ margin?: FlexProps['margin'];
+};
+
+// TODO some config map number => DeviceModelInternal
+const getModelEnumFromBytesUtil = (_id: number) => {
+ return DeviceModelInternal.T3W1;
+};
+
+// TODO some config map number => color id
+// discuss final format of it
+const getColorEnumFromVariantBytesUtil = (variant: number) => {
+ return variant;
+};
+
+export const BluetoothDevice = ({ device, flex, margin }: BluetoothDeviceProps) => {
+ const model = getModelEnumFromBytesUtil(device.internal_model);
+ const color = getColorEnumFromVariantBytesUtil(device.model_variant);
+ const colorName = models[model].colors[color.toString()];
+
+ return (
+
+
+
+
+ Trezor Safe 7
+
+
+
+ {colorName}
+
+
+
+ {device.name}
+
+
+
+
+ );
+};
diff --git a/packages/suite/src/components/suite/bluetooth/BluetoothDeviceItem.tsx b/packages/suite/src/components/suite/bluetooth/BluetoothDeviceItem.tsx
new file mode 100644
index 000000000000..767f63508bb7
--- /dev/null
+++ b/packages/suite/src/components/suite/bluetooth/BluetoothDeviceItem.tsx
@@ -0,0 +1,39 @@
+import { ElectronBluetoothDevice } from '@trezor/suite-desktop-api';
+import { Button, Row } from '@trezor/components';
+import { spacings } from '@trezor/theme';
+
+import { BluetoothDevice } from './BluetoothDevice';
+import { DeviceBluetoothStatusType } from './types';
+
+type BluetoothDeviceItemProps = {
+ device: ElectronBluetoothDevice;
+ onClick: () => void;
+ connecting: any;
+ lastSeenTimestamp: any;
+ selectedDeviceStatusType?: DeviceBluetoothStatusType;
+};
+
+export const BluetoothDeviceItem = ({ device, onClick }: BluetoothDeviceItemProps) => {
+ // const timestamp = device.timestamp
+ // ? new Date(device.timestamp * 1000).toLocaleTimeString('en-US', { hour12: false })
+ // : 'Unknown';
+
+ // const lastSeenInSec = Math.floor((Date.now() - lastSeenTimestamp) / 1000);
+ // const seenQuiteLongAgo = lastSeenInSec > 5;
+
+ // Last seen: {timestamp}
+ //
+ // Paired: {device.paired ? 'yes' : 'no'}, Pairing mode:{' '}
+ // {device.pairable ? 'yes' : 'no'}, Signal strength: {device.rssi}
+ //
+ // {seenQuiteLongAgo && Last seen: {lastSeenInSec}s ago}
+
+ return (
+
+
+
+
+ );
+};
diff --git a/packages/suite/src/components/suite/bluetooth/BluetoothDeviceList.tsx b/packages/suite/src/components/suite/bluetooth/BluetoothDeviceList.tsx
new file mode 100644
index 000000000000..e486ed8618c0
--- /dev/null
+++ b/packages/suite/src/components/suite/bluetooth/BluetoothDeviceList.tsx
@@ -0,0 +1,45 @@
+import { Card, Column, SkeletonRectangle, Row } from '@trezor/components';
+import { spacings } from '@trezor/theme';
+import { ElectronBluetoothDevice } from '@trezor/suite-desktop-api';
+
+import { BluetoothDeviceItem } from './BluetoothDeviceItem';
+
+type BluetoothDeviceListProps = {
+ deviceList: ElectronBluetoothDevice[];
+ onSelect: (uuid: string) => void;
+ isScanning: boolean;
+};
+
+const SkeletonDevice = () => (
+
+
+
+
+
+
+
+
+);
+
+export const BluetoothDeviceList = ({
+ onSelect,
+ deviceList,
+ isScanning,
+}: BluetoothDeviceListProps) => {
+ return (
+
+
+ {deviceList.map(d => (
+ onSelect(d.uuid)}
+ connecting={false}
+ lastSeenTimestamp={0}
+ />
+ ))}
+ {isScanning && }
+
+
+ );
+};
diff --git a/packages/suite/src/components/suite/bluetooth/BluetoothPairingPin.tsx b/packages/suite/src/components/suite/bluetooth/BluetoothPairingPin.tsx
new file mode 100644
index 000000000000..ad130d5b617b
--- /dev/null
+++ b/packages/suite/src/components/suite/bluetooth/BluetoothPairingPin.tsx
@@ -0,0 +1,59 @@
+import styled from 'styled-components';
+
+import { Row, NewModal, Card } from '@trezor/components';
+import { spacings, spacingsPx, typography } from '@trezor/theme';
+import { ElectronBluetoothDevice } from '@trezor/suite-desktop-api';
+
+import { BluetoothDevice } from './BluetoothDevice';
+
+const Pin = styled.div`
+ display: flex;
+ flex: 1;
+
+ ${typography.titleLarge} /* Amount */ margin: 0 auto;
+
+ letter-spacing: ${spacingsPx.md};
+`;
+
+type BluetoothPairingPinProps = {
+ onCancel: () => void;
+ onConfirm: () => void;
+ pairingPin?: string;
+ device: ElectronBluetoothDevice;
+};
+
+export const BluetoothPairingPin = ({
+ onConfirm,
+ onCancel,
+ pairingPin,
+ device,
+}: BluetoothPairingPinProps) => (
+
+ Connect
+
+ Cancel
+
+ >
+ }
+ >
+
+
+ {pairingPin}
+
+
+
+
+);
diff --git a/packages/suite/src/components/suite/bluetooth/BluetoothScanFooter.tsx b/packages/suite/src/components/suite/bluetooth/BluetoothScanFooter.tsx
new file mode 100644
index 000000000000..a0739e89712f
--- /dev/null
+++ b/packages/suite/src/components/suite/bluetooth/BluetoothScanFooter.tsx
@@ -0,0 +1,31 @@
+import { Text } from '@trezor/components';
+import { spacings } from '@trezor/theme';
+
+import { NotTrezorYouAreLookingFor } from './NotTrezorYouAreLookingFor';
+import { FakeScanStatus } from './types';
+
+type BluetoothScanFooterProps = {
+ onReScanClick: () => void;
+ fakeScanStatus: FakeScanStatus;
+ numberOfDevices: number;
+};
+
+export const BluetoothScanFooter = ({
+ onReScanClick,
+ fakeScanStatus,
+ numberOfDevices,
+}: BluetoothScanFooterProps) => {
+ if (fakeScanStatus === 'running') {
+ return (
+
+ Make sure your TS7 is on and in pairing mode (hold power button)
+
+ );
+ }
+
+ if (fakeScanStatus === 'done' && numberOfDevices > 0) {
+ return ;
+ }
+
+ return null;
+};
diff --git a/packages/suite/src/components/suite/bluetooth/BluetoothScanHeader.tsx b/packages/suite/src/components/suite/bluetooth/BluetoothScanHeader.tsx
new file mode 100644
index 000000000000..43eb14b8ac68
--- /dev/null
+++ b/packages/suite/src/components/suite/bluetooth/BluetoothScanHeader.tsx
@@ -0,0 +1,35 @@
+import { spacings } from '@trezor/theme';
+import { Button, Row, Spinner, Text } from '@trezor/components';
+
+type BluetoothScanHeaderProps = {
+ isScanning: boolean;
+ numberOfDevices: number;
+ onClose: () => void;
+};
+
+export const BluetoothScanHeader = ({
+ isScanning,
+ onClose,
+ numberOfDevices,
+}: BluetoothScanHeaderProps) => (
+
+
+ {isScanning ? (
+ <>
+
+ Scanning
+ >
+ ) : (
+
+ {numberOfDevices} Trezors Found
+
+ )}
+
+
+
+);
diff --git a/packages/suite/src/components/suite/bluetooth/BluetoothSelectedDevice.tsx b/packages/suite/src/components/suite/bluetooth/BluetoothSelectedDevice.tsx
new file mode 100644
index 000000000000..394fa4e46f18
--- /dev/null
+++ b/packages/suite/src/components/suite/bluetooth/BluetoothSelectedDevice.tsx
@@ -0,0 +1,79 @@
+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';
+
+export type BluetoothSelectedDeviceProps = {
+ device: ElectronBluetoothDevice;
+ status: DeviceBluetoothStatus;
+ onCancel: () => void;
+};
+
+export const BluetoothSelectedDevice = ({
+ device,
+ status,
+ onCancel,
+}: BluetoothSelectedDeviceProps) => {
+ const map: Record ReactNode) | null> = {
+ found: null,
+ error: () => (
+
+
+ Error
+
+ ),
+ pairing: () => (
+
+
+ Pairing
+
+ ),
+ paired: () => (
+
+
+ Paired
+
+ ),
+ connecting: () => (
+
+
+ Connecting
+
+ ),
+
+ // Todo: here we shall solve how to continue with Trezor Host Protocol
+ connected: () => (
+
+
+ Waiting
+
+ ),
+ };
+
+ if (status.type === 'pairing' && (status.pin?.length ?? 0) > 0) {
+ return (
+ {}}
+ />
+ );
+ }
+
+ return (
+
+
+
+
+ {map[status.type]?.()}
+
+
+
+ );
+};
diff --git a/packages/suite/src/components/suite/bluetooth/BluetoothTips.tsx b/packages/suite/src/components/suite/bluetooth/BluetoothTips.tsx
new file mode 100644
index 000000000000..8ca392e2bda9
--- /dev/null
+++ b/packages/suite/src/components/suite/bluetooth/BluetoothTips.tsx
@@ -0,0 +1,55 @@
+import { Button, Card, Column, Divider, Icon, IconName, Row, Text } from '@trezor/components';
+import { spacings } from '@trezor/theme';
+
+type BluetoothTipProps = {
+ icon: IconName;
+ header: string;
+ text: string;
+};
+
+const BluetoothTip = ({ icon, header, text }: BluetoothTipProps) => (
+
+
+
+ {header}
+
+ {text}
+
+
+
+);
+
+type BluetoothTipsProps = {
+ onReScanClick: () => void;
+};
+
+export const BluetoothTips = ({ onReScanClick }: BluetoothTipsProps) => (
+
+
+
+ Check tips & try again
+
+
+
+
+
+
+
+
+
+
+);
diff --git a/packages/suite/src/components/suite/bluetooth/NotTrezorYouAreLookingFor.tsx b/packages/suite/src/components/suite/bluetooth/NotTrezorYouAreLookingFor.tsx
new file mode 100644
index 000000000000..9ce8ab25a60a
--- /dev/null
+++ b/packages/suite/src/components/suite/bluetooth/NotTrezorYouAreLookingFor.tsx
@@ -0,0 +1,37 @@
+import { useState } from 'react';
+
+import { Card, CollapsibleBox, ElevationUp, Link, Text } from '@trezor/components';
+import { spacings } from '@trezor/theme';
+
+import { BluetoothTips } from './BluetoothTips';
+
+type NotTrezorYouAreLookingForProps = {
+ onReScanClick: () => void;
+};
+
+export const NotTrezorYouAreLookingFor = ({ onReScanClick }: NotTrezorYouAreLookingForProps) => {
+ const [showTips, setShowTips] = useState(false);
+
+ return (
+ setShowTips(true)}>
+ Not the Trezor you’re looking for?
+
+ }
+ >
+ {showTips && (
+
+
+
+
+
+ )}
+
+ );
+};
diff --git a/packages/suite/src/components/suite/bluetooth/errors/BluetoothDeniedForSuite.tsx b/packages/suite/src/components/suite/bluetooth/errors/BluetoothDeniedForSuite.tsx
new file mode 100644
index 000000000000..af5d8257121d
--- /dev/null
+++ b/packages/suite/src/components/suite/bluetooth/errors/BluetoothDeniedForSuite.tsx
@@ -0,0 +1,41 @@
+import { useState } from 'react';
+
+import { Text, NewModal, Column, Banner } from '@trezor/components';
+import { desktopApi } from '@trezor/suite-desktop-api';
+import { spacings } from '@trezor/theme';
+
+type BluetoothDeniedForSuiteProps = {
+ onCancel: () => void;
+};
+
+export const BluetoothDeniedForSuite = ({ onCancel }: BluetoothNotEnabledProps) => {
+ const [hasDeeplinkFailed, setHasDeeplinkFailed] = useState(false);
+
+ return (
+
+ Enable bluetooth
+
+ Cancel
+
+ >
+ }
+ >
+
+ Enable bluetooth on your computer
+
+ Or connect your Trezor via cable.
+
+ {hasDeeplinkFailed && (
+
+ Cannot open bluetooth settings. Please enable bluetooth manually.
+
+ )}
+
+
+ );
+};
diff --git a/packages/suite/src/components/suite/bluetooth/errors/BluetoothNotAllowedForSuite.tsx b/packages/suite/src/components/suite/bluetooth/errors/BluetoothNotAllowedForSuite.tsx
new file mode 100644
index 000000000000..591b2b8185b9
--- /dev/null
+++ b/packages/suite/src/components/suite/bluetooth/errors/BluetoothNotAllowedForSuite.tsx
@@ -0,0 +1,3 @@
+export const BluetoothNotAllowedForSuite = () => {
+ return <>>;
+};
diff --git a/packages/suite/src/components/suite/bluetooth/errors/BluetoothNotEnabled.tsx b/packages/suite/src/components/suite/bluetooth/errors/BluetoothNotEnabled.tsx
new file mode 100644
index 000000000000..cf868df515ae
--- /dev/null
+++ b/packages/suite/src/components/suite/bluetooth/errors/BluetoothNotEnabled.tsx
@@ -0,0 +1,51 @@
+import { useState } from 'react';
+
+import { Text, NewModal, Column, Banner } from '@trezor/components';
+import { desktopApi } from '@trezor/suite-desktop-api';
+import { spacings } from '@trezor/theme';
+
+type BluetoothNotEnabledProps = {
+ onCancel: () => void;
+};
+
+export const BluetoothNotEnabled = ({ onCancel }: BluetoothNotEnabledProps) => {
+ const [hasDeeplinkFailed, setHasDeeplinkFailed] = useState(false);
+
+ const openSettings = async () => {
+ const opened = await desktopApi.bluetoothOpenSettings();
+
+ console.log('opened', opened);
+
+ if (!opened.success || !opened.payload) {
+ setHasDeeplinkFailed(true);
+ }
+ };
+
+ return (
+
+ Enable bluetooth
+
+ Cancel
+
+ >
+ }
+ >
+
+ Enable bluetooth on your computer
+
+ Or connect your Trezor via cable.
+
+ {hasDeeplinkFailed && (
+
+ Cannot open bluetooth settings. Please enable bluetooth manually.
+
+ )}
+
+
+ );
+};
diff --git a/packages/suite/src/components/suite/bluetooth/errors/BluetoothVersionNotCompatible.tsx b/packages/suite/src/components/suite/bluetooth/errors/BluetoothVersionNotCompatible.tsx
new file mode 100644
index 000000000000..1c176f682cfb
--- /dev/null
+++ b/packages/suite/src/components/suite/bluetooth/errors/BluetoothVersionNotCompatible.tsx
@@ -0,0 +1,30 @@
+import { Text, NewModal, Column } from '@trezor/components';
+import { spacings } from '@trezor/theme';
+
+type BluetoothVersionNotCompatibleProps = {
+ onCancel: () => void;
+};
+
+export const BluetoothVersionNotCompatible = ({ onCancel }: BluetoothVersionNotCompatibleProps) => (
+
+
+ Cancel
+
+ >
+ }
+ >
+
+
+ Your computer’s bluetooth version is not compatible with whatever we’re using copy.
+
+
+ Use cable, buy a 5.0+ dongle.
+
+
+
+);
diff --git a/packages/suite/src/components/suite/bluetooth/types.ts b/packages/suite/src/components/suite/bluetooth/types.ts
new file mode 100644
index 000000000000..0582e037f34f
--- /dev/null
+++ b/packages/suite/src/components/suite/bluetooth/types.ts
@@ -0,0 +1,12 @@
+import { BluetoothDeviceConnectionStatus } from '@trezor/suite-desktop-api';
+
+export type FakeScanStatus = 'running' | 'done';
+
+export type DeviceBluetoothStatus =
+ | BluetoothDeviceConnectionStatus
+ | {
+ uuid: string;
+ type: 'found' | 'error';
+ };
+
+export type DeviceBluetoothStatusType = DeviceBluetoothStatus['type'];
diff --git a/packages/transport-test/package.json b/packages/transport-test/package.json
index e8bc7f282e64..9a98460e8ca4 100644
--- a/packages/transport-test/package.json
+++ b/packages/transport-test/package.json
@@ -13,7 +13,7 @@
"test:e2e:new-bridge:hw": "USE_HW=true USE_NODE_BRIDGE=true yarn test:e2e:bridge",
"test:e2e:new-bridge:emu": "USE_HW=false USE_NODE_BRIDGE=true yarn test:e2e:bridge",
"build:e2e:api:node": "yarn esbuild ./e2e/api/api.test.ts --bundle --outfile=./e2e/dist/api.test.node.js --platform=node --target=node18 --external:usb",
- "build:e2e:api:browser": "yarn esbuild ./e2e/api/api.test.ts --bundle --outfile=./e2e/dist/api.test.browser.js --platform=browser --external:usb && cp e2e/ui/api.test.html e2e/dist/index.html",
+ "build:e2e:api:browser": "yarn esbuild ./e2e/api/api.test.ts --bundle --log-level=verbose --outfile=./e2e/dist/api.test.browser.js --platform=browser --external:usb && cp e2e/ui/api.test.html e2e/dist/index.html",
"test:e2e:api:node:hw": "yarn build:e2e:api:node && node ./e2e/dist/api.test.node.js",
"test:e2e:api:browser:hw": "yarn build:e2e:api:browser && npx http-serve ./e2e/dist"
},