From bae5507260c730a038e8d6d85a0c07e2ae26abaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20V=C3=A1clav=C3=ADk?= Date: Thu, 18 Apr 2024 16:23:49 +0200 Subject: [PATCH] WIP --- .../HotkeyBadge/HotkeyBadge.stories.tsx | 20 ++ .../components/HotkeyBadge/HotkeyBadge.tsx | 56 +++++ packages/components/src/index.ts | 1 + .../suite/layouts/SuiteLayout/SuiteLayout.tsx | 4 +- .../suite/layouts/SuiteLayout/utils.ts | 19 +- .../modals/ModalSwitcher/DiscoveryLoader.tsx | 43 ++-- .../ModalSwitcher/DiscoveryLoaderLegacy.tsx | 26 +++ .../modals/ModalSwitcher/ModalSwitcher.tsx | 9 +- .../DeviceContextModal/DeviceContextModal.tsx | 10 +- .../DeviceContextModal/PassphraseModal.tsx | 155 ++++++++------ .../PassphraseModalLegacy.tsx | 199 ++++++++++++++++++ .../src/components/suite/modals/index.tsx | 1 + .../suite/SwitchDevice/CardWithDevice.tsx | 64 ++++++ .../DeviceItem/AddWalletButton.tsx | 12 +- .../SwitchDevice/DeviceItem/DeviceHeader.tsx | 74 +++++++ .../SwitchDevice/DeviceItem/DeviceItem.tsx | 158 ++++---------- ...viceHeaderButton.tsx => DeviceWarning.tsx} | 6 +- .../SwitchDevice/DeviceItem/EjectButton.tsx | 39 ++++ .../SwitchDevice/DeviceItem/ViewOnly.tsx | 98 ++++----- .../DeviceItem/ViewOnlyRadios.tsx | 26 +-- .../DeviceItem/WalletInstance.tsx | 3 +- .../views/suite/SwitchDevice/SwitchDevice.tsx | 51 +---- .../suite/SwitchDevice/SwitchDeviceModal.tsx | 43 +++- 23 files changed, 784 insertions(+), 333 deletions(-) create mode 100644 packages/components/src/components/HotkeyBadge/HotkeyBadge.stories.tsx create mode 100644 packages/components/src/components/HotkeyBadge/HotkeyBadge.tsx create mode 100644 packages/suite/src/components/suite/modals/ModalSwitcher/DiscoveryLoaderLegacy.tsx create mode 100644 packages/suite/src/components/suite/modals/ReduxModal/DeviceContextModal/PassphraseModalLegacy.tsx create mode 100644 packages/suite/src/views/suite/SwitchDevice/CardWithDevice.tsx create mode 100644 packages/suite/src/views/suite/SwitchDevice/DeviceItem/DeviceHeader.tsx rename packages/suite/src/views/suite/SwitchDevice/DeviceItem/{DeviceHeaderButton.tsx => DeviceWarning.tsx} (90%) create mode 100644 packages/suite/src/views/suite/SwitchDevice/DeviceItem/EjectButton.tsx diff --git a/packages/components/src/components/HotkeyBadge/HotkeyBadge.stories.tsx b/packages/components/src/components/HotkeyBadge/HotkeyBadge.stories.tsx new file mode 100644 index 000000000000..3cc4bfad255c --- /dev/null +++ b/packages/components/src/components/HotkeyBadge/HotkeyBadge.stories.tsx @@ -0,0 +1,20 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { HotkeyBadge as HotkeyBadgeComponent, HotkeyBadgeProps } from './HotkeyBadge'; + +const meta: Meta = { + title: 'Form/HotkeyBadge', + component: HotkeyBadgeComponent, +} as Meta; +export default meta; + +const Component = ({ children, ...args }: HotkeyBadgeProps) => { + return {children}; +}; + +export const HotkeyBadge: StoryObj = { + render: Component, + args: { + children: 'CMD + P', + isActive: true, + }, +}; diff --git a/packages/components/src/components/HotkeyBadge/HotkeyBadge.tsx b/packages/components/src/components/HotkeyBadge/HotkeyBadge.tsx new file mode 100644 index 000000000000..013b01fb8421 --- /dev/null +++ b/packages/components/src/components/HotkeyBadge/HotkeyBadge.tsx @@ -0,0 +1,56 @@ +import { + Elevation, + borders, + mapElevationToBackground, + spacingsPx, + typography, +} from '@trezor/theme'; +import styled from 'styled-components'; +import { ElevationDown, useElevation } from '../ElevationContext/ElevationContext'; + +export const Container = styled.div<{ $elevation: Elevation; $isActive: boolean }>` + display: flex; + gap: ${spacingsPx.xxs}; + align-items: center; + justify-content: center; + background: ${mapElevationToBackground}; + border-radius: ${borders.radii.xs}; + color: ${({ theme }) => theme.textSubdued}; + opacity: ${({ $isActive }) => ($isActive ? 1 : 0.5)}; + user-select: none; + padding: 0 ${spacingsPx.xxs}; + ${typography.label} + position: relative; +`; + +export interface HotkeyBadgeProps { + isActive?: boolean; + children: string; +} +const Component = ({ isActive = true, children }: HotkeyBadgeProps) => { + const { elevation } = useElevation(); + console.log('______elevation', elevation); + + const isMac = navigator.userAgent.includes('Macintosh'); + + const split = children.split(/(\+)/g).map(x => x.trim()); + const hotkeyReplaces: Record = { + MOD: isMac ? '⌘' : 'CTRL', + }; + + return ( + + {split.map(hotkey => ( + {hotkeyReplaces[hotkey] || hotkey} + ))} + + ); +}; + +export const HotkeyBadge = (props: HotkeyBadgeProps) => { + return ( + + + + ); +}; diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 5dfa1ebe15f6..52dfae6701ef 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -29,6 +29,7 @@ export * from './components/DataAnalytics'; export * from './components/Divider/Divider'; export * from './components/Dropdown/Dropdown'; export * from './components/ElevationContext/ElevationContext'; +export * from './components/HotkeyBadge/HotkeyBadge'; export * from './components/form/Input/Input'; export * from './components/form/InputStyles'; export * from './components/form/Radio/Radio'; diff --git a/packages/suite/src/components/suite/layouts/SuiteLayout/SuiteLayout.tsx b/packages/suite/src/components/suite/layouts/SuiteLayout/SuiteLayout.tsx index a000c45846a1..776e8d727335 100644 --- a/packages/suite/src/components/suite/layouts/SuiteLayout/SuiteLayout.tsx +++ b/packages/suite/src/components/suite/layouts/SuiteLayout/SuiteLayout.tsx @@ -19,9 +19,9 @@ import { Sidebar } from './Sidebar/Sidebar'; import { CoinjoinBars } from './CoinjoinBars/CoinjoinBars'; import { MobileAccountsMenu } from 'src/components/wallet/WalletLayout/AccountsMenu/MobileAccountsMenu'; import { selectSelectedAccount } from 'src/reducers/wallet/selectedAccountReducer'; +import { useAppShortcuts } from './utils'; export const SCROLL_WRAPPER_ID = 'layout-scroll'; - export const Wrapper = styled.div` display: flex; flex: 1; @@ -100,6 +100,8 @@ export const SuiteLayout = ({ children }: SuiteLayoutProps) => { const isAccountPage = !!selectedAccount; + useAppShortcuts(); + return ( diff --git a/packages/suite/src/components/suite/layouts/SuiteLayout/utils.ts b/packages/suite/src/components/suite/layouts/SuiteLayout/utils.ts index 0305f076eeb2..3e1ce8a1ea23 100644 --- a/packages/suite/src/components/suite/layouts/SuiteLayout/utils.ts +++ b/packages/suite/src/components/suite/layouts/SuiteLayout/utils.ts @@ -1,5 +1,7 @@ +import { createDeviceInstance, selectDevice } from '@suite-common/wallet-core'; +import { useEvent } from 'react-use'; import { useCustomBackends } from 'src/hooks/settings/backends'; -import { useSelector } from 'src/hooks/suite'; +import { useDispatch, useSelector } from 'src/hooks/suite'; export const useEnabledBackends = () => { const enabledNetworks = useSelector(state => state.wallet.settings.enabledNetworks); @@ -7,3 +9,18 @@ export const useEnabledBackends = () => { return customBackends.filter(backend => enabledNetworks.includes(backend.coin)); }; + +export const useAppShortcuts = () => { + const device = useSelector(selectDevice); + const dispatch = useDispatch(); + + useEvent('keydown', e => { + const modKey = e.metaKey; // CMD or Ctrl key + + // press CMD + P to show PassphraseModal + if (modKey && e.key === 'p' && device) { + dispatch(createDeviceInstance({ device })); + e.preventDefault(); // prevent Print dialog + } + }); +}; diff --git a/packages/suite/src/components/suite/modals/ModalSwitcher/DiscoveryLoader.tsx b/packages/suite/src/components/suite/modals/ModalSwitcher/DiscoveryLoader.tsx index 50ddb225712b..6dc2bbacd48e 100644 --- a/packages/suite/src/components/suite/modals/ModalSwitcher/DiscoveryLoader.tsx +++ b/packages/suite/src/components/suite/modals/ModalSwitcher/DiscoveryLoader.tsx @@ -1,26 +1,37 @@ import styled from 'styled-components'; -import { Spinner } from '@trezor/components'; -import { Translation, Modal } from 'src/components/suite'; +import { H3, Spinner, Text } from '@trezor/components'; +import { Translation } from 'src/components/suite'; +import { CardWithDevice } from 'src/views/suite/SwitchDevice/CardWithDevice'; +import { SwitchDeviceRenderer } from 'src/views/suite/SwitchDevice/SwitchDeviceRenderer'; +import { useSelector } from 'src/hooks/suite'; +import { selectDevice } from '@suite-common/wallet-core'; const Expand = styled.div` display: flex; + flex-direction: column; width: 100%; justify-content: center; + align-items: center; margin: 40px 0; `; -const StyledModal = styled(Modal)` - width: 360px; -`; +export const DiscoveryLoader = () => { + const device = useSelector(selectDevice); + if (!device) return null; -export const DiscoveryLoader = () => ( - } - description={} - data-test="@discovery/loader" - > - - - - -); + return ( + + + + +

+ +

+ + + +
+
+
+ ); +}; diff --git a/packages/suite/src/components/suite/modals/ModalSwitcher/DiscoveryLoaderLegacy.tsx b/packages/suite/src/components/suite/modals/ModalSwitcher/DiscoveryLoaderLegacy.tsx new file mode 100644 index 000000000000..a1a2fd69af7b --- /dev/null +++ b/packages/suite/src/components/suite/modals/ModalSwitcher/DiscoveryLoaderLegacy.tsx @@ -0,0 +1,26 @@ +import styled from 'styled-components'; +import { Spinner } from '@trezor/components'; +import { Translation, Modal } from 'src/components/suite'; + +const Expand = styled.div` + display: flex; + width: 100%; + justify-content: center; + margin: 40px 0; +`; + +const StyledModal = styled(Modal)` + width: 360px; +`; + +export const DiscoveryLoaderLegacy = () => ( + } + description={} + data-test="@discovery/loader" + > + + + + +); diff --git a/packages/suite/src/components/suite/modals/ModalSwitcher/ModalSwitcher.tsx b/packages/suite/src/components/suite/modals/ModalSwitcher/ModalSwitcher.tsx index acd442787cd2..d4c630b33327 100644 --- a/packages/suite/src/components/suite/modals/ModalSwitcher/ModalSwitcher.tsx +++ b/packages/suite/src/components/suite/modals/ModalSwitcher/ModalSwitcher.tsx @@ -2,17 +2,24 @@ import { usePreferredModal } from 'src/hooks/suite/usePreferredModal'; import { ReduxModal } from '../ReduxModal/ReduxModal'; import { ForegroundAppModal } from './ForegroundAppModal'; import { DiscoveryLoader } from './DiscoveryLoader'; +import { useSelector } from 'src/hooks/suite'; +import { DiscoveryLoaderLegacy } from './DiscoveryLoaderLegacy'; /** Displays whichever redux modal or foreground app should be displayed */ export const ModalSwitcher = () => { + const isViewOnlyModeVisible = useSelector( + state => state.suite.settings.debug.isViewOnlyModeVisible, + ); const modal = usePreferredModal(); + + return ; switch (modal.type) { case 'foreground-app': return ; case 'redux-modal': return ; case 'discovery-loading': - return ; + return isViewOnlyModeVisible ? : ; default: return null; } diff --git a/packages/suite/src/components/suite/modals/ReduxModal/DeviceContextModal/DeviceContextModal.tsx b/packages/suite/src/components/suite/modals/ReduxModal/DeviceContextModal/DeviceContextModal.tsx index f6727735d970..8aa57f1ddc01 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/DeviceContextModal/DeviceContextModal.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/DeviceContextModal/DeviceContextModal.tsx @@ -9,6 +9,7 @@ import { PinModal, PinInvalidModal, PassphraseModal, + PassphraseModalLegacy, PassphraseSourceModal, PassphraseOnDeviceModal, ConfirmActionModal, @@ -29,6 +30,9 @@ export const DeviceContextModal = ({ }: ReduxModalProps) => { const device = useSelector(selectDevice); const intl = useIntl(); + const isViewOnlyModeVisible = useSelector( + state => state.suite.settings.debug.isViewOnlyModeVisible, + ); if (!device) return null; @@ -44,7 +48,11 @@ export const DeviceContextModal = ({ // Passphrase on host case UI.REQUEST_PASSPHRASE: - return ; + return isViewOnlyModeVisible ? ( + + ) : ( + + ); case 'WordRequestType_Plain': return ; diff --git a/packages/suite/src/components/suite/modals/ReduxModal/DeviceContextModal/PassphraseModal.tsx b/packages/suite/src/components/suite/modals/ReduxModal/DeviceContextModal/PassphraseModal.tsx index c989666ac7de..89ae02a79d9e 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/DeviceContextModal/PassphraseModal.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/DeviceContextModal/PassphraseModal.tsx @@ -3,20 +3,23 @@ import { useIntl } from 'react-intl'; import styled from 'styled-components'; -import { variables, PassphraseTypeCard } from '@trezor/components'; +import { variables, PassphraseTypeCard, Card, H3, Text } from '@trezor/components'; import TrezorConnect from '@trezor/connect'; import * as deviceUtils from '@suite-common/suite-utils'; import { selectIsDiscoveryAuthConfirmationRequired, selectDevices, onPassphraseSubmit, + selectDevice, } from '@suite-common/wallet-core'; - +import { DeviceStatus } from 'src/components/suite/layouts/SuiteLayout/DeviceSelector/DeviceStatus'; import { useSelector, useDispatch } from 'src/hooks/suite'; import { Translation, Modal } from 'src/components/suite'; import type { TrezorDevice } from 'src/types/suite'; import { OpenGuideFromTooltip } from 'src/components/guide'; import messages from 'src/support/messages'; +import { SwitchDeviceRenderer } from 'src/views/suite/SwitchDevice/SwitchDeviceRenderer'; +import { CardWithDevice } from 'src/views/suite/SwitchDevice/CardWithDevice'; const Wrapper = styled.div<{ $authConfirmation?: boolean }>` display: flex; @@ -53,8 +56,11 @@ interface PassphraseModalProps { } export const PassphraseModal = ({ device }: PassphraseModalProps) => { + console.log('_____TU'); const [submitted, setSubmitted] = useState(false); const devices = useSelector(selectDevices); + const deviceModelInternal = device.features?.internal_model; + const selectedDevice = useSelector(selectDevice); const authConfirmation = useSelector(selectIsDiscoveryAuthConfirmationRequired) || device.authConfirm; @@ -95,7 +101,8 @@ export const PassphraseModal = ({ device }: PassphraseModalProps) => { if (authConfirmation || stateConfirmation) { // show borderless one-column modal for confirming passphrase and state confirmation return ( - + {/* @@ -113,76 +120,51 @@ export const PassphraseModal = ({ device }: PassphraseModalProps) => { ) } - > - } - offerPassphraseOnDevice={onDeviceOffer} - onSubmit={onSubmit} - singleColModal - learnMoreTooltipOnClick={ - - } - /> - + > */} + + } + offerPassphraseOnDevice={onDeviceOffer} + onSubmit={onSubmit} + singleColModal + learnMoreTooltipOnClick={ + + } + /> + + ); } // creating a hidden wallet if (!noPassphraseOffer) { return ( - + +

+ +

+ + + + + {/* } description={} isCancelable onCancel={onCancel} - > - } - description={} - submitLabel={} - type="hidden" - singleColModal - offerPassphraseOnDevice={onDeviceOffer} - onSubmit={onSubmit} - learnMoreTooltipOnClick={ - - } - /> - - ); - } - - // show 2-column modal for selecting between standard and hidden wallets - return ( - } - > - - - } - description={} - submitLabel={} - type="standard" - onSubmit={onSubmit} - /> - + > */} } description={} - submitLabel={} + submitLabel={} type="hidden" + singleColModal offerPassphraseOnDevice={onDeviceOffer} onSubmit={onSubmit} learnMoreTooltipOnClick={ @@ -192,8 +174,57 @@ export const PassphraseModal = ({ device }: PassphraseModalProps) => { /> } /> - - - + {/*
*/} + + + ); + } + + // @TODO create standard wallet here? + onSubmit(''); + + return null; + + // show 2-column modal for selecting between standard and hidden wallets + return ( + + + {/* } + > */} + + + } + description={} + submitLabel={} + type="standard" + onSubmit={onSubmit} + /> + + } + description={} + submitLabel={ + + } + type="hidden" + offerPassphraseOnDevice={onDeviceOffer} + onSubmit={onSubmit} + learnMoreTooltipOnClick={ + + } + /> + + + {/* */} + + ); }; diff --git a/packages/suite/src/components/suite/modals/ReduxModal/DeviceContextModal/PassphraseModalLegacy.tsx b/packages/suite/src/components/suite/modals/ReduxModal/DeviceContextModal/PassphraseModalLegacy.tsx new file mode 100644 index 000000000000..6e6156706438 --- /dev/null +++ b/packages/suite/src/components/suite/modals/ReduxModal/DeviceContextModal/PassphraseModalLegacy.tsx @@ -0,0 +1,199 @@ +import { useCallback, useState } from 'react'; +import { useIntl } from 'react-intl'; + +import styled from 'styled-components'; + +import { variables, PassphraseTypeCard } from '@trezor/components'; +import TrezorConnect from '@trezor/connect'; +import * as deviceUtils from '@suite-common/suite-utils'; +import { + selectIsDiscoveryAuthConfirmationRequired, + selectDevices, + onPassphraseSubmit, +} from '@suite-common/wallet-core'; + +import { useSelector, useDispatch } from 'src/hooks/suite'; +import { Translation, Modal } from 'src/components/suite'; +import type { TrezorDevice } from 'src/types/suite'; +import { OpenGuideFromTooltip } from 'src/components/guide'; +import messages from 'src/support/messages'; + +const Wrapper = styled.div<{ $authConfirmation?: boolean }>` + display: flex; + flex-direction: column; + align-items: center; + + @media screen and (max-width: ${variables.SCREEN_SIZE.MD}) { + width: 100%; + } +`; + +const WalletsWrapper = styled.div` + display: flex; + flex-direction: column; + width: 100%; +`; + +const Divider = styled.div` + margin: 16px; + height: 1px; + background: ${({ theme }) => theme.STROKE_GREY}; +`; + +const TinyModal = styled(Modal)` + width: 450px; +`; + +const SmallModal = styled(Modal)` + width: 600px; +`; + +interface PassphraseModalProps { + device: TrezorDevice; +} + +export const PassphraseModalLegacy = ({ device }: PassphraseModalProps) => { + const [submitted, setSubmitted] = useState(false); + const devices = useSelector(selectDevices); + const authConfirmation = + useSelector(selectIsDiscoveryAuthConfirmationRequired) || device.authConfirm; + + const stateConfirmation = !!device.state; + const hasEmptyPassphraseWallet = deviceUtils + .getDeviceInstances(device, devices) + .find(d => d.useEmptyPassphrase); + const noPassphraseOffer = !hasEmptyPassphraseWallet && !stateConfirmation; + const onDeviceOffer = !!( + device.features && + device.features.capabilities && + device.features.capabilities.includes('Capability_PassphraseEntry') + ); + + const dispatch = useDispatch(); + + const intl = useIntl(); + + const onCancel = () => TrezorConnect.cancel(intl.formatMessage(messages.TR_CANCELLED)); + + const onSubmit = useCallback( + (value: string, passphraseOnDevice?: boolean) => { + setSubmitted(true); + dispatch(onPassphraseSubmit({ value, passphraseOnDevice: !!passphraseOnDevice })); + }, + [setSubmitted, dispatch], + ); + + const onRecreate = useCallback(() => { + // Cancel TrezorConnect request and pass error to suiteAction.authConfirm + TrezorConnect.cancel('auth-confirm-cancel'); + }, []); + + if (submitted) { + return null; + } + + if (authConfirmation || stateConfirmation) { + // show borderless one-column modal for confirming passphrase and state confirmation + return ( + + ) : ( + + ) + } + isCancelable + onCancel={onCancel} + onBackClick={authConfirmation ? onRecreate : undefined} + description={ + !authConfirmation ? ( + + ) : ( + + ) + } + > + } + offerPassphraseOnDevice={onDeviceOffer} + onSubmit={onSubmit} + singleColModal + learnMoreTooltipOnClick={ + + } + /> + + ); + } + + // creating a hidden wallet + if (!noPassphraseOffer) { + return ( + } + description={} + isCancelable + onCancel={onCancel} + > + } + description={} + submitLabel={} + type="hidden" + singleColModal + offerPassphraseOnDevice={onDeviceOffer} + onSubmit={onSubmit} + learnMoreTooltipOnClick={ + + } + /> + + ); + } + + // show 2-column modal for selecting between standard and hidden wallets + return ( + } + > + + + } + description={} + submitLabel={} + type="standard" + onSubmit={onSubmit} + /> + + } + description={} + submitLabel={} + type="hidden" + offerPassphraseOnDevice={onDeviceOffer} + onSubmit={onSubmit} + learnMoreTooltipOnClick={ + + } + /> + + + + ); +}; diff --git a/packages/suite/src/components/suite/modals/index.tsx b/packages/suite/src/components/suite/modals/index.tsx index 1faae571c2b4..fb653597b1e6 100644 --- a/packages/suite/src/components/suite/modals/index.tsx +++ b/packages/suite/src/components/suite/modals/index.tsx @@ -2,6 +2,7 @@ export { PinModal } from './ReduxModal/DeviceContextModal/PinModal'; export { PinInvalidModal } from './ReduxModal/DeviceContextModal/PinInvalidModal'; export { PinMismatchModal } from './ReduxModal/UserContextModal/PinMismatchModal'; export { PassphraseModal } from './ReduxModal/DeviceContextModal/PassphraseModal'; +export { PassphraseModalLegacy } from './ReduxModal/DeviceContextModal/PassphraseModalLegacy'; export { PassphraseSourceModal } from './ReduxModal/DeviceContextModal/PassphraseSourceModal'; export { PassphraseOnDeviceModal } from './ReduxModal/DeviceContextModal/PassphraseOnDeviceModal'; export { PassphraseDuplicateModal } from './ReduxModal/UserContextModal/PassphraseDuplicateModal'; diff --git a/packages/suite/src/views/suite/SwitchDevice/CardWithDevice.tsx b/packages/suite/src/views/suite/SwitchDevice/CardWithDevice.tsx new file mode 100644 index 000000000000..e2f53323021b --- /dev/null +++ b/packages/suite/src/views/suite/SwitchDevice/CardWithDevice.tsx @@ -0,0 +1,64 @@ +import { ReactNode, useState } from 'react'; + +import { AnimatePresence, motion } from 'framer-motion'; +import styled from 'styled-components'; +import { Card, motionAnimation } from '@trezor/components'; +import * as deviceUtils from '@suite-common/suite-utils'; + +import type { TrezorDevice, ForegroundAppProps } from 'src/types/suite'; +import { spacingsPx } from '@trezor/theme'; + +import { DeviceHeader } from './DeviceItem/DeviceHeader'; + +const DeviceWrapper = styled.div` + display: flex; + flex-direction: column; + width: 100%; + gap: ${spacingsPx.xs}; + + & + & { + margin-top: ${spacingsPx.xxxl}; + } +`; + +interface CardWithDeviceProps { + children: ReactNode; + deviceWarning?: ReactNode; + onCancel?: ForegroundAppProps['onCancel']; + device: TrezorDevice; +} + +export const CardWithDevice = ({ + children, + onCancel, + device, + deviceWarning, +}: CardWithDeviceProps) => { + const deviceStatus = deviceUtils.getStatus(device); + const [isExpanded, setIsExpanded] = useState(true); + + const needsAttention = deviceUtils.deviceNeedsAttention(deviceStatus); + const isUnknown = device.type !== 'acquired'; + + return ( + + + + {deviceWarning} + + {!needsAttention && ( + + {!isUnknown && isExpanded && ( + {children} + )} + + )} + + + ); +}; diff --git a/packages/suite/src/views/suite/SwitchDevice/DeviceItem/AddWalletButton.tsx b/packages/suite/src/views/suite/SwitchDevice/DeviceItem/AddWalletButton.tsx index 39852ff268af..7480d1c8aea0 100644 --- a/packages/suite/src/views/suite/SwitchDevice/DeviceItem/AddWalletButton.tsx +++ b/packages/suite/src/views/suite/SwitchDevice/DeviceItem/AddWalletButton.tsx @@ -1,11 +1,12 @@ import styled from 'styled-components'; -import { Button, Tooltip } from '@trezor/components'; +import { Button, HotkeyBadge, Tooltip } from '@trezor/components'; import { Translation } from 'src/components/suite'; import { TrezorDevice, AcquiredDevice } from 'src/types/suite'; import { useSelector } from 'src/hooks/suite'; import { SUITE } from 'src/actions/suite/constants'; +import { spacingsPx } from '@trezor/theme'; const AddWallet = styled.div` display: flex; @@ -16,6 +17,10 @@ const AddWallet = styled.div` const StyledTooltip = styled(Tooltip)` width: 100%; `; +const Flex = styled.div` + display: flex; + gap: ${spacingsPx.xs}; +`; interface AddWalletButtonProps { device: TrezorDevice; @@ -72,7 +77,10 @@ export const AddWalletButton = ({ onClick={onAddWallet} > {emptyPassphraseWalletExists ? ( - + + + MOD + P + ) : ( )} diff --git a/packages/suite/src/views/suite/SwitchDevice/DeviceItem/DeviceHeader.tsx b/packages/suite/src/views/suite/SwitchDevice/DeviceItem/DeviceHeader.tsx new file mode 100644 index 000000000000..d51b54b81415 --- /dev/null +++ b/packages/suite/src/views/suite/SwitchDevice/DeviceItem/DeviceHeader.tsx @@ -0,0 +1,74 @@ +import styled, { useTheme } from 'styled-components'; +import { Icon } from '@trezor/components'; +import { DeviceStatus } from 'src/components/suite/layouts/SuiteLayout/DeviceSelector/DeviceStatus'; +import { isWebUsb } from 'src/utils/suite/transport'; +import { WebUsbButton } from 'src/components/suite'; +import { spacingsPx } from '@trezor/theme'; +import { useSelector } from 'src/hooks/suite'; +import { motion } from 'framer-motion'; +import { ForegroundAppProps, TrezorDevice } from 'src/types/suite'; +import { selectDevice } from '@suite-common/wallet-core'; + +const Container = styled.div` + display: flex; + align-items: center; + flex: 1; + cursor: pointer; +`; + +const DeviceActions = styled.div` + display: flex; + align-items: center; + margin-left: ${spacingsPx.lg}; + gap: ${spacingsPx.xxs}; +`; + +interface DeviceHeaderProps { + device: TrezorDevice; + onCancel?: ForegroundAppProps['onCancel']; + setIsExpanded: (expanded: boolean) => void; + isExpanded: boolean; +} + +export const DeviceHeader = ({ + onCancel, + device, + setIsExpanded, + isExpanded, +}: DeviceHeaderProps) => { + const transport = useSelector(state => state.suite.transport); + const isWebUsbTransport = isWebUsb(transport); + const theme = useTheme(); + const deviceModelInternal = device.features?.internal_model; + const selectedDevice = useSelector(selectDevice); + + return ( + onCancel?.()}> + {deviceModelInternal && ( + + )} + + + {isWebUsbTransport && } + + { + setIsExpanded(!isExpanded); + e.stopPropagation(); + }} + /> + + + + ); +}; diff --git a/packages/suite/src/views/suite/SwitchDevice/DeviceItem/DeviceItem.tsx b/packages/suite/src/views/suite/SwitchDevice/DeviceItem/DeviceItem.tsx index d4bb33a53223..8d7ae6044f43 100644 --- a/packages/suite/src/views/suite/SwitchDevice/DeviceItem/DeviceItem.tsx +++ b/packages/suite/src/views/suite/SwitchDevice/DeviceItem/DeviceItem.tsx @@ -1,52 +1,20 @@ -import { useState } from 'react'; - -import { AnimatePresence, motion } from 'framer-motion'; -import styled, { useTheme } from 'styled-components'; -import { variables, Icon, motionAnimation } from '@trezor/components'; +import styled from 'styled-components'; +import { variables } from '@trezor/components'; import * as deviceUtils from '@suite-common/suite-utils'; -import { - selectDevice, - acquireDevice, - createDeviceInstance, - selectDeviceThunk, -} from '@suite-common/wallet-core'; +import { selectDevice, createDeviceInstance, selectDeviceThunk } from '@suite-common/wallet-core'; import { useDispatch, useSelector } from 'src/hooks/suite'; import { goto } from 'src/actions/suite/routerActions'; import { WalletInstance } from './WalletInstance'; import { AddWalletButton } from './AddWalletButton'; -import { DeviceHeaderButton } from './DeviceHeaderButton'; +import { acquireDevice } from '@suite-common/wallet-core'; import type { TrezorDevice, AcquiredDevice, ForegroundAppProps } from 'src/types/suite'; import type { getBackgroundRoute } from 'src/utils/suite/router'; import { spacingsPx } from '@trezor/theme'; -import { DeviceStatus } from 'src/components/suite/layouts/SuiteLayout/DeviceSelector/DeviceStatus'; -import { isWebUsb } from 'src/utils/suite/transport'; -import { WebUsbButton } from 'src/components/suite'; - -const DeviceWrapper = styled.div` - display: flex; - flex-direction: column; - width: 100%; - gap: ${spacingsPx.xs}; - - & + & { - margin-top: ${spacingsPx.xxxl}; - } -`; - -const Device = styled.div` - display: flex; - align-items: center; -`; - -const DeviceActions = styled.div` - display: flex; - align-items: center; - margin-left: ${spacingsPx.lg}; - gap: ${spacingsPx.xxs}; -`; +import { CardWithDevice } from '../CardWithDevice'; +import { DeviceWarning } from './DeviceWarning'; const WalletsWrapper = styled.div<{ $enabled: boolean }>` opacity: ${({ $enabled }) => ($enabled ? 1 : 0.5)}; @@ -64,13 +32,6 @@ const InstancesWrapper = styled.div` gap: ${spacingsPx.xs}; `; -const DeviceHeader = styled.div` - display: flex; - align-items: center; - flex: 1; - cursor: pointer; -`; - interface DeviceItemProps { device: TrezorDevice; instances: AcquiredDevice[]; @@ -81,17 +42,8 @@ interface DeviceItemProps { export const DeviceItem = ({ device, instances, onCancel, backgroundRoute }: DeviceItemProps) => { const selectedDevice = useSelector(selectDevice); const dispatch = useDispatch(); - const transport = useSelector(state => state.suite.transport); - - const isWebUsbTransport = isWebUsb(transport); - const theme = useTheme(); - const [isExpanded, setIsExpanded] = useState(true); - const deviceStatus = deviceUtils.getStatus(device); - const deviceModelInternal = device.features?.internal_model; - const needsAttention = deviceUtils.deviceNeedsAttention(deviceStatus); - const isUnknown = device.type !== 'acquired'; const instancesWithState = instances.filter(i => i.state); const handleRedirection = async () => { @@ -136,70 +88,38 @@ export const DeviceItem = ({ device, instances, onCancel, backgroundRoute }: Dev }; return ( - - - onCancel()}> - {deviceModelInternal && ( - - )} - - - {isWebUsbTransport && } - - setIsExpanded(!isExpanded)} - /> - - - - - - {!needsAttention && ( - - {!isUnknown && isExpanded && ( - - - - {instancesWithState.map((instance, index) => ( - - ))} - - - - - - )} - - )} - + + } + onCancel={onCancel} + device={device} + > + + + {instancesWithState.map((instance, index) => ( + + ))} + + + + + ); }; diff --git a/packages/suite/src/views/suite/SwitchDevice/DeviceItem/DeviceHeaderButton.tsx b/packages/suite/src/views/suite/SwitchDevice/DeviceItem/DeviceWarning.tsx similarity index 90% rename from packages/suite/src/views/suite/SwitchDevice/DeviceItem/DeviceHeaderButton.tsx rename to packages/suite/src/views/suite/SwitchDevice/DeviceItem/DeviceWarning.tsx index 3edce5e2846d..d70d6933ab3d 100644 --- a/packages/suite/src/views/suite/SwitchDevice/DeviceItem/DeviceHeaderButton.tsx +++ b/packages/suite/src/views/suite/SwitchDevice/DeviceItem/DeviceWarning.tsx @@ -3,17 +3,17 @@ import * as deviceUtils from '@suite-common/suite-utils'; import { NotificationCard, Translation } from 'src/components/suite'; import { TrezorDevice } from 'src/types/suite'; -interface DeviceHeaderButtonProps { +interface DeviceWarningProps { needsAttention: boolean; device: TrezorDevice; onSolveIssueClick: () => void; } -export const DeviceHeaderButton = ({ +export const DeviceWarning = ({ device, needsAttention, onSolveIssueClick, -}: DeviceHeaderButtonProps) => { +}: DeviceWarningProps) => { const deviceStatus = deviceUtils.getStatus(device); const deviceStatusMessage = deviceUtils.getDeviceNeedsAttentionMessage(deviceStatus); diff --git a/packages/suite/src/views/suite/SwitchDevice/DeviceItem/EjectButton.tsx b/packages/suite/src/views/suite/SwitchDevice/DeviceItem/EjectButton.tsx new file mode 100644 index 000000000000..08e6d0eecc74 --- /dev/null +++ b/packages/suite/src/views/suite/SwitchDevice/DeviceItem/EjectButton.tsx @@ -0,0 +1,39 @@ +import styled, { useTheme } from 'styled-components'; + +import { Tooltip, Icon } from '@trezor/components'; +import { Translation } from 'src/components/suite'; +import { spacingsPx } from '@trezor/theme'; +import { ContentType } from '../types'; + +const EjectContainer = styled.div` + position: absolute; + right: ${spacingsPx.sm}; + top: ${spacingsPx.sm}; +`; + +interface EjectButtonProps { + setContentType: (contentType: ContentType) => void; + dataTest?: string; +} + +export const EjectButton = ({ setContentType, dataTest }: EjectButtonProps) => { + const theme = useTheme(); + + return ( + + }> + { + setContentType('ejectConfirmation'); + e.stopPropagation(); + }} + /> + + + ); +}; diff --git a/packages/suite/src/views/suite/SwitchDevice/DeviceItem/ViewOnly.tsx b/packages/suite/src/views/suite/SwitchDevice/DeviceItem/ViewOnly.tsx index f7e26bd2dd00..69ed582b1eef 100644 --- a/packages/suite/src/views/suite/SwitchDevice/DeviceItem/ViewOnly.tsx +++ b/packages/suite/src/views/suite/SwitchDevice/DeviceItem/ViewOnly.tsx @@ -1,6 +1,6 @@ -import styled, { useTheme } from 'styled-components'; +import styled from 'styled-components'; -import { CollapsibleBox, Text, Tooltip, Icon } from '@trezor/components'; +import { CollapsibleBox, Text } from '@trezor/components'; import { Translation } from 'src/components/suite'; import { ViewOnlyRadios } from './ViewOnlyRadios'; import { spacingsPx } from '@trezor/theme'; @@ -25,12 +25,6 @@ const ViewOnlyContent = styled.div` align-items: center; `; -const EjectContainer = styled.div` - position: absolute; - right: ${spacingsPx.sm}; - top: ${spacingsPx.sm}; -`; - const Circle = styled.div<{ $isHighlighted?: boolean }>` width: 6px; height: 6px; @@ -42,72 +36,52 @@ const Circle = styled.div<{ $isHighlighted?: boolean }>` export const ViewOnly = ({ setContentType, instance, dataTest }: ViewOnlyProps) => { const [isViewOnlyExpanded, setIsViewOnlyExpanded] = useState(false); const dispatch = useDispatch(); - const theme = useTheme(); + const isViewOnly = !!instance.remember; - const handleRememberChange = (value: boolean) => { + const handleRememberChange = () => { setContentType('default'); - setIsViewOnlyExpanded(false); dispatch( toggleRememberDevice({ device: instance, - forceRemember: value === true ? true : undefined, }), ); }; return ( - <> - { - e.stopPropagation(); - }} + { + e.stopPropagation(); + }} + > + setIsViewOnlyExpanded(!isViewOnlyExpanded)} + heading={ + + + + {isViewOnly ? ( + + ) : ( + + )} + + + } > - setIsViewOnlyExpanded(!isViewOnlyExpanded)} - heading={ - - - - {isViewOnly ? ( - - ) : ( - - )} - - - } - > - - - - - - }> - { - setContentType('ejectConfirmation'); - e.stopPropagation(); - }} - /> - - - + + + ); }; diff --git a/packages/suite/src/views/suite/SwitchDevice/DeviceItem/ViewOnlyRadios.tsx b/packages/suite/src/views/suite/SwitchDevice/DeviceItem/ViewOnlyRadios.tsx index 32cf00be81ca..fb1a9a7ebcef 100644 --- a/packages/suite/src/views/suite/SwitchDevice/DeviceItem/ViewOnlyRadios.tsx +++ b/packages/suite/src/views/suite/SwitchDevice/DeviceItem/ViewOnlyRadios.tsx @@ -3,11 +3,10 @@ import styled from 'styled-components'; import { Text, Radio, Button, Icon, useElevation } from '@trezor/components'; import { Elevation, borders, mapElevationToBorder, spacingsPx, typography } from '@trezor/theme'; import { Translation } from 'src/components/suite'; -import { useState } from 'react'; type ViewOnlyRadiosProps = { isViewOnlyActive: boolean; - setIsViewOnlyActive: (isViewOnlyActive: boolean) => void; + toggleViewOnly: () => void; dataTest?: string; }; type ViewOnlyRadioProps = { @@ -75,20 +74,23 @@ export const ViewOnlyRadio = ({ }; export const ViewOnlyRadios = ({ isViewOnlyActive, - setIsViewOnlyActive, + toggleViewOnly, dataTest, }: ViewOnlyRadiosProps) => { - const [isViewOnlyActiveTemp, setIsViewOnlyActiveTemp] = useState(isViewOnlyActive); - const handleConfirm = () => { - setIsViewOnlyActive(isViewOnlyActiveTemp); + // const [isViewOnlyActiveTemp, setIsViewOnlyActiveTemp] = useState(isViewOnlyActive); + const handleConfirm = (newValue: boolean) => { + const isValueChanged = isViewOnlyActive !== newValue; + if (isValueChanged) { + toggleViewOnly(); + } }; return ( } - onClick={() => setIsViewOnlyActiveTemp(true)} - isChecked={isViewOnlyActiveTemp} + onClick={() => handleConfirm(true)} + isChecked={isViewOnlyActive} dataTest={`${dataTest}/enabled`} > } - onClick={() => setIsViewOnlyActiveTemp(false)} - isChecked={!isViewOnlyActiveTemp} + onClick={() => handleConfirm(false)} + isChecked={!isViewOnlyActive} dataTest={`${dataTest}/disabled`} > - + {/* - + */} diff --git a/packages/suite/src/views/suite/SwitchDevice/DeviceItem/WalletInstance.tsx b/packages/suite/src/views/suite/SwitchDevice/DeviceItem/WalletInstance.tsx index 5749c80a2880..69df092e29c3 100644 --- a/packages/suite/src/views/suite/SwitchDevice/DeviceItem/WalletInstance.tsx +++ b/packages/suite/src/views/suite/SwitchDevice/DeviceItem/WalletInstance.tsx @@ -22,6 +22,7 @@ import { useState } from 'react'; import { EjectConfirmation } from './EjectConfirmation'; import { ContentType } from '../types'; import { ViewOnly } from './ViewOnly'; +import { EjectButton } from './EjectButton'; const InstanceType = styled.div<{ isSelected: boolean }>` display: flex; @@ -111,6 +112,7 @@ export const WalletInstance = ({ {...rest} > {isSelected && } + {discoveryProcess && ( @@ -163,7 +165,6 @@ export const WalletInstance = ({ instance={instance} /> )} - {contentType === 'ejectConfirmation' && ( { const selectedDevice = useSelector(selectDevice); @@ -38,37 +25,17 @@ export const SwitchDevice = ({ cancelable, onCancel }: ForegroundAppProps) => { const backgroundRoute = getBackgroundRoute(); - const initial = { - width: 279, - height: 70, - }; - return ( - - - <> - {sortedDevices.map(device => ( - - - - ))} - - - + {sortedDevices.map(device => ( + + ))} ); }; diff --git a/packages/suite/src/views/suite/SwitchDevice/SwitchDeviceModal.tsx b/packages/suite/src/views/suite/SwitchDevice/SwitchDeviceModal.tsx index bb9b3c319857..40e73a7b15ca 100644 --- a/packages/suite/src/views/suite/SwitchDevice/SwitchDeviceModal.tsx +++ b/packages/suite/src/views/suite/SwitchDevice/SwitchDeviceModal.tsx @@ -1,7 +1,7 @@ -import { ElevationContext } from '@trezor/components'; import { useEvent } from 'react-use'; import styled from 'styled-components'; - +import { motion } from 'framer-motion'; +import { spacingsPx } from '@trezor/theme'; type SwitchDeviceModalProps = { children?: React.ReactNode; isCancelable?: boolean; @@ -15,6 +15,19 @@ const Container = styled.div` margin: 5px; `; +const DeviceItemsWrapper = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + gap: ${spacingsPx.md}; + flex: 1; +`; + +const initial = { + width: 279, + height: 70, +}; + export const SwitchDeviceModal = ({ children, onCancel, @@ -27,13 +40,23 @@ export const SwitchDeviceModal = ({ }); return ( - - e.stopPropagation()} // needed because of the Backdrop implementation - data-test={dataTest} - > - {children} - - + e.stopPropagation()} // needed because of the Backdrop implementation + data-test={dataTest} + > + + + {children} + + + ); };