From 9bdd3acc56ae6bca64c375dc70c290230291f325 Mon Sep 17 00:00:00 2001 From: Peter Sanderson Date: Wed, 27 Nov 2024 14:48:34 +0100 Subject: [PATCH] WIP: design 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 --- .../skeletons/SkeletonRectangle.tsx | 9 +- .../RotateDeviceImage/RotateDeviceImage.tsx | 9 +- .../src/modules/bluetooth.ts | 106 +++++------ .../PrerequisitesGuide/DeviceConnect.tsx | 28 ++- .../PrerequisitesGuide/PrerequisitesGuide.tsx | 78 +++++--- .../suite/bluetooth/BluetoothConnect.tsx | 177 ++++++++++++++++++ .../suite/bluetooth/BluetoothDevice.tsx | 54 ++++++ .../suite/bluetooth/BluetoothDeviceItem.tsx | 39 ++++ .../suite/bluetooth/BluetoothDeviceList.tsx | 45 +++++ .../suite/bluetooth/BluetoothPairingPin.tsx | 59 ++++++ .../suite/bluetooth/BluetoothScanFooter.tsx | 31 +++ .../suite/bluetooth/BluetoothScanHeader.tsx | 35 ++++ .../bluetooth/BluetoothSelectedDevice.tsx | 79 ++++++++ .../suite/bluetooth/BluetoothTips.tsx | 55 ++++++ .../bluetooth/NotTrezorYouAreLookingFor.tsx | 37 ++++ .../errors/BluetoothDeniedForSuite.tsx | 41 ++++ .../errors/BluetoothNotAllowedForSuite.tsx | 3 + .../bluetooth/errors/BluetoothNotEnabled.tsx | 51 +++++ .../errors/BluetoothVersionNotCompatible.tsx | 30 +++ .../src/components/suite/bluetooth/types.ts | 12 ++ packages/transport-test/package.json | 2 +- 21 files changed, 889 insertions(+), 91 deletions(-) create mode 100644 packages/suite/src/components/suite/bluetooth/BluetoothConnect.tsx create mode 100644 packages/suite/src/components/suite/bluetooth/BluetoothDevice.tsx create mode 100644 packages/suite/src/components/suite/bluetooth/BluetoothDeviceItem.tsx create mode 100644 packages/suite/src/components/suite/bluetooth/BluetoothDeviceList.tsx create mode 100644 packages/suite/src/components/suite/bluetooth/BluetoothPairingPin.tsx create mode 100644 packages/suite/src/components/suite/bluetooth/BluetoothScanFooter.tsx create mode 100644 packages/suite/src/components/suite/bluetooth/BluetoothScanHeader.tsx create mode 100644 packages/suite/src/components/suite/bluetooth/BluetoothSelectedDevice.tsx create mode 100644 packages/suite/src/components/suite/bluetooth/BluetoothTips.tsx create mode 100644 packages/suite/src/components/suite/bluetooth/NotTrezorYouAreLookingFor.tsx create mode 100644 packages/suite/src/components/suite/bluetooth/errors/BluetoothDeniedForSuite.tsx create mode 100644 packages/suite/src/components/suite/bluetooth/errors/BluetoothNotAllowedForSuite.tsx create mode 100644 packages/suite/src/components/suite/bluetooth/errors/BluetoothNotEnabled.tsx create mode 100644 packages/suite/src/components/suite/bluetooth/errors/BluetoothVersionNotCompatible.tsx create mode 100644 packages/suite/src/components/suite/bluetooth/types.ts 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" },