diff --git a/packages/components/src/components/Passphrase/EnterOnTrezorButton.tsx b/packages/components/src/components/Passphrase/EnterOnTrezorButton.tsx new file mode 100644 index 000000000000..72132251fc46 --- /dev/null +++ b/packages/components/src/components/Passphrase/EnterOnTrezorButton.tsx @@ -0,0 +1,47 @@ +import { FormattedMessage } from 'react-intl'; +import styled from 'styled-components'; +import { Image } from '../Image/Image'; +import { DeviceModelInternal } from '@trezor/connect'; +import { Card } from '../Card/Card'; +import { Icon } from '../assets/Icon/Icon'; +import { spacingsPx } from '@trezor/theme'; + +const Row = styled.div` + display: flex; + gap: ${spacingsPx.xs}; + align-items: center; + justify-content: space-between; +`; + +interface EnterOnTrezorButtonProps { + submit: (value: string, passphraseOnDevice?: boolean) => void; + isVisible: boolean; + value: string; + deviceModel?: DeviceModelInternal; +} + +export const EnterOnTrezorButton = ({ + submit, + isVisible, + value, + deviceModel, +}: EnterOnTrezorButtonProps) => { + if (!isVisible) return null; + + return ( + submit(value, true)} + data-test="@passphrase/enter-on-device-button" + > + + {deviceModel && Trezor} + + + + + ); +}; diff --git a/packages/components/src/components/Passphrase/PassphraseTypeCard.tsx b/packages/components/src/components/Passphrase/PassphraseTypeCard.tsx index 53b79ae10888..b82f47f7763a 100644 --- a/packages/components/src/components/Passphrase/PassphraseTypeCard.tsx +++ b/packages/components/src/components/Passphrase/PassphraseTypeCard.tsx @@ -19,6 +19,9 @@ import { Checkbox } from '../form/Checkbox/Checkbox'; import { Input } from '../form/Input/Input'; import { Icon } from '../assets/Icon/Icon'; import { TooltipProps, Tooltip } from '../Tooltip/Tooltip'; +import { EnterOnTrezorButton } from './EnterOnTrezorButton'; +import { Card } from '../Card/Card'; +import { DeviceModelInternal } from '@trezor/connect'; type WalletType = 'standard' | 'hidden'; @@ -27,11 +30,12 @@ type WrapperProps = { $singleColModal?: boolean; }; +const Item = styled.div``; + const Wrapper = styled.div` display: flex; flex: 1; - - /* align-items: center; */ + gap: ${spacingsPx.xs}; border-radius: ${borders.radii.xs}; flex-direction: column; text-align: left; @@ -137,23 +141,6 @@ const ActionButton = styled(Button)` } `; -const OnDeviceActionButton = styled(ActionButton)` - background: transparent; - text-decoration: underline; - color: ${({ theme }) => theme.textSubdued}; - - &:first-child { - margin-top: 0; - } - - &:hover, - &:focus, - &:active { - color: ${({ theme }) => theme.textSubdued}; - background: transparent; - } -`; - const Content = styled.div` display: flex; flex: 1; @@ -170,6 +157,7 @@ export type PassphraseTypeCardProps = { offerPassphraseOnDevice?: boolean; singleColModal?: boolean; authConfirmation?: boolean; + deviceModel?: DeviceModelInternal | null; onSubmit: (value: string, passphraseOnDevice?: boolean) => void; learnMoreTooltipOnClick?: TooltipProps['addon']; learnMoreTooltipAppendTo?: TooltipProps['appendTo']; @@ -277,157 +265,172 @@ export const PassphraseTypeCard = (props: PassphraseTypeCardProps) => { }} data-test={`@passphrase-type/${props.type}`} > - {!props.singleColModal && ( - // only used to show options in modal where user selects wallet type - // single col modal such as one for creating hidden wallet shows only input and submit button - <> - - - {props.type === 'standard' ? ( - - ) : ( - + + {!props.singleColModal && ( + // only used to show options in modal where user selects wallet type + // single col modal such as one for creating hidden wallet shows only input and submit button + <> + + + {props.type === 'standard' ? ( + + ) : ( + + )} + + + + {props.type === 'hidden' ? ( + + } + addon={props.learnMoreTooltipOnClick} + content={ + + } + dashed + > + <>{props.title} + + ) : ( + props.title + )} + + {props.description} + + {props.type === 'standard' && ( + + + )} - - - - {props.type === 'hidden' ? ( - + + {props.type === 'hidden' && } + + )} + + + + {props.type === 'hidden' && ( + <> + + {/* Show passphrase input */} + + + ) : null } - addon={props.learnMoreTooltipOnClick} - content={ - { + if ( + typeof ref.current?.selectionStart === + 'number' + ) { + caretRef.current = + ref.current.selectionStart; + } + setShowPassword(!showPassword); + }} + data-test="@passphrase/show-toggle" /> } - dashed - > - <>{props.title} - - ) : ( - props.title - )} - - {props.description} - - {props.type === 'standard' && ( - - - - )} - - {props.type === 'hidden' && } - - )} - {props.type === 'hidden' && ( - <> - - {/* Show passphrase input */} - - - ) : null - } - inputState={isTooLong ? 'error' : undefined} - autoFocus={!isAndroid()} - innerAddon={ - { - if (typeof ref.current?.selectionStart === 'number') { - caretRef.current = ref.current.selectionStart; - } - setShowPassword(!showPassword); - }} - data-test="@passphrase/show-toggle" /> - } - /> - - - {!isTooLong && } - - )} - {props.authConfirmation && ( - // Checkbox if user fully understands what's happening when confirming empty passphrase - - setEnabled(!enabled)} - isChecked={enabled} - > - - - - )} - - {props.type === 'hidden' && ( - - {/* Submit button */} - {/* Visible in standalone modal for creating a hidden wallet or after a click also in modal for selecting wallet type */} - {(props.singleColModal || hiddenWalletTouched) && ( - - submit(value)} - isFullWidth - > - {props.submitLabel} - - - )} - {/* Offer entering passphrase on a device */} - {props.offerPassphraseOnDevice && ( - submit(value, true)} - isFullWidth - data-test="@passphrase/enter-on-device-button" + + + {!isTooLong && } + + )} + {props.authConfirmation && ( + // Checkbox if user fully understands what's happening when confirming empty passphrase + + setEnabled(!enabled)} + isChecked={enabled} > - + + + )} + + + {props.type === 'hidden' && ( + + {/* Submit button */} + {/* Visible in standalone modal for creating a hidden wallet or after a click also in modal for selecting wallet type */} + {(props.singleColModal || hiddenWalletTouched) && ( + + submit(value)} + isFullWidth + > + {props.submitLabel} + + + )} + )} - - )} - + + + + + {/* Offer entering passphrase on a device */} + + {props.offerPassphraseOnDevice && ( + + )} + + ); }; diff --git a/packages/components/src/components/buttons/Button/Button.stories.tsx b/packages/components/src/components/buttons/Button/Button.stories.tsx index 791f8a9c10c4..c11b381da94b 100644 --- a/packages/components/src/components/buttons/Button/Button.stories.tsx +++ b/packages/components/src/components/buttons/Button/Button.stories.tsx @@ -10,5 +10,13 @@ export default meta; export const Button: StoryObj = { args: { children: 'Button label', + margin: { top: undefined, right: undefined, bottom: undefined, left: undefined }, + }, + argTypes: { + margin: { + table: { + category: 'Frame props', + }, + }, }, }; diff --git a/packages/components/src/components/buttons/Button/Button.tsx b/packages/components/src/components/buttons/Button/Button.tsx index 9bf1d214435c..bae88af49045 100644 --- a/packages/components/src/components/buttons/Button/Button.tsx +++ b/packages/components/src/components/buttons/Button/Button.tsx @@ -14,15 +14,17 @@ import { } from '../buttonStyleUtils'; import { focusStyleTransition, getFocusShadowStyle } from '../../../utils/utils'; import { useElevation } from '../../ElevationContext/ElevationContext'; +import { makePropsTransient } from '../../../utils/transientProps'; +import { FrameProps, TransientFrameProps, withFrameProps } from '../../common/frameProps'; -interface ButtonContainerProps { +type ButtonContainerProps = TransientFrameProps & { $variant: ButtonVariant; $size: ButtonSize; $iconAlignment?: IconAlignment; $hasIcon?: boolean; $isFullWidth?: boolean; $elevation: Elevation; -} +}; export const ButtonContainer = styled.button` display: flex; @@ -49,6 +51,8 @@ export const ButtonContainer = styled.button` pointer-events: none; cursor: default; } + + ${withFrameProps} `; interface ContentProps { @@ -80,20 +84,21 @@ type SelectedHTMLButtonProps = Pick< 'onClick' | 'onMouseOver' | 'onMouseLeave' | 'type' | 'tabIndex' >; -export interface ButtonProps extends SelectedHTMLButtonProps { - variant?: ButtonVariant; - size?: ButtonSize; - isDisabled?: boolean; - isLoading?: boolean; - isFullWidth?: boolean; - icon?: IconType; - iconSize?: number; - iconAlignment?: IconAlignment; - children: React.ReactNode; - title?: string; - className?: string; - 'data-test'?: string; -} +export type ButtonProps = SelectedHTMLButtonProps & + FrameProps & { + variant?: ButtonVariant; + size?: ButtonSize; + isDisabled?: boolean; + isLoading?: boolean; + isFullWidth?: boolean; + icon?: IconType; + iconSize?: number; + iconAlignment?: IconAlignment; + children: React.ReactNode; + title?: string; + className?: string; + 'data-test'?: string; + }; export const Button = ({ variant = 'primary', @@ -106,8 +111,13 @@ export const Button = ({ iconAlignment = 'left', type = 'button', children, + margin, ...rest }: ButtonProps) => { + const frameProps = { + margin, + }; + const theme = useTheme(); const { elevation } = useElevation(); @@ -132,6 +142,7 @@ export const Button = ({ $hasIcon={!!icon || isLoading} $elevation={elevation} {...rest} + {...makePropsTransient(frameProps)} > {!isLoading && icon && IconComponent} {isLoading && Loader} diff --git a/packages/components/src/components/typography/Heading/Heading.tsx b/packages/components/src/components/typography/Heading/Heading.tsx index de148a3cc955..4913093fddd2 100644 --- a/packages/components/src/components/typography/Heading/Heading.tsx +++ b/packages/components/src/components/typography/Heading/Heading.tsx @@ -1,17 +1,35 @@ import styled from 'styled-components'; -import { typography } from '@trezor/theme'; +import { TypographyStyle, typography } from '@trezor/theme'; +import { FrameProps, TransientFrameProps, withFrameProps } from '../../common/frameProps'; +import { makePropsTransient } from '../../../utils/transientProps'; -const H1 = styled.h1` - ${typography.titleLarge}; -`; +type HeadingProps = TransientFrameProps & { + children: string; + $typographyStyle: TypographyStyle; +}; -const H2 = styled.h2` - ${typography.titleMedium}; -`; +const Heading = styled.h1` + ${({ $typographyStyle }) => typography[$typographyStyle]}; -const H3 = styled.h3` - ${typography.titleSmall}; + ${withFrameProps} `; -export { H1, H2, H3 }; +type HProps = FrameProps & { + children: React.ReactNode; +}; + +export const H1 = ({ margin, ...props }: HProps) => ( + +); +export const H2 = ({ margin, ...props }: HProps) => ( + +); +export const H3 = ({ margin, ...props }: HProps) => ( + +); 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..3d6c6ce5eed7 100644 --- a/packages/suite/src/components/suite/layouts/SuiteLayout/utils.ts +++ b/packages/suite/src/components/suite/layouts/SuiteLayout/utils.ts @@ -1,5 +1,8 @@ +import { createDeviceInstance, selectDevice } from '@suite-common/wallet-core'; +import { useEvent } from 'react-use'; +import { goto } from 'src/actions/suite/routerActions'; 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 +10,30 @@ 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 default behaviour + } + + // press CMD + D to show SwitchDevice + if (modKey && e.key === 'd' && device) { + dispatch( + goto('suite-switch-device', { + params: { + cancelable: true, + }, + }), + ); + e.preventDefault(); // prevent default behaviour + } + }); +}; 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..e9fc18b95de3 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 ; // @TODO remove 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/PassphraseDescription.tsx b/packages/suite/src/components/suite/modals/ReduxModal/DeviceContextModal/PassphraseDescription.tsx new file mode 100644 index 000000000000..d9a819899db0 --- /dev/null +++ b/packages/suite/src/components/suite/modals/ReduxModal/DeviceContextModal/PassphraseDescription.tsx @@ -0,0 +1,21 @@ +import { Icon, Text } from '@trezor/components'; +import { PassphraseList, PassphraseItem } from './PassphraseList'; + +export const PassphraseDescription = () => { + return ( + + + + Important to first learn how passphrase works + + + + Passphrase opens a wallet secured by that phrase + + + + No one can recover it, not even Trezor support + + + ); +}; diff --git a/packages/suite/src/components/suite/modals/ReduxModal/DeviceContextModal/PassphraseHeading.tsx b/packages/suite/src/components/suite/modals/ReduxModal/DeviceContextModal/PassphraseHeading.tsx new file mode 100644 index 000000000000..19414f1d8882 --- /dev/null +++ b/packages/suite/src/components/suite/modals/ReduxModal/DeviceContextModal/PassphraseHeading.tsx @@ -0,0 +1,8 @@ +import { H3 } from '@trezor/components'; + +type PassphraseHeadingProps = { + children: React.ReactNode; +}; +export const PassphraseHeading = ({ children }: PassphraseHeadingProps) => ( +

{children}

+); diff --git a/packages/suite/src/components/suite/modals/ReduxModal/DeviceContextModal/PassphraseList.tsx b/packages/suite/src/components/suite/modals/ReduxModal/DeviceContextModal/PassphraseList.tsx new file mode 100644 index 000000000000..88a5106683ff --- /dev/null +++ b/packages/suite/src/components/suite/modals/ReduxModal/DeviceContextModal/PassphraseList.tsx @@ -0,0 +1,16 @@ +import { spacingsPx, typography } from '@trezor/theme'; +import styled from 'styled-components'; + +export const PassphraseList = styled.div` + display: flex; + flex-direction: column; + gap: ${spacingsPx.sm}; + margin-top: ${spacingsPx.xs}; + margin-bottom: ${spacingsPx.md}; + justify-content: center; +`; +export const PassphraseItem = styled.div` + display: flex; + ${typography.hint}; + gap: ${spacingsPx.xs}; +`; 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..c5b17037ae90 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/DeviceContextModal/PassphraseModal.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/DeviceContextModal/PassphraseModal.tsx @@ -1,52 +1,25 @@ import { useCallback, useState } from 'react'; import { useIntl } from 'react-intl'; -import styled from 'styled-components'; - -import { variables, PassphraseTypeCard } from '@trezor/components'; +import { PassphraseTypeCard } from '@trezor/components'; import TrezorConnect from '@trezor/connect'; import * as deviceUtils from '@suite-common/suite-utils'; import { selectIsDiscoveryAuthConfirmationRequired, selectDevices, onPassphraseSubmit, + selectDeviceModel, } from '@suite-common/wallet-core'; - import { useSelector, useDispatch } from 'src/hooks/suite'; -import { Translation, Modal } from 'src/components/suite'; +import { Translation } 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; -`; +import { SwitchDeviceRenderer } from 'src/views/suite/SwitchDevice/SwitchDeviceRenderer'; +import { CardWithDevice } from 'src/views/suite/SwitchDevice/CardWithDevice'; +import { PassphraseDescription } from './PassphraseDescription'; +import { PassphraseWalletConfirmation } from './PassphraseWalletConfirmation'; +import { PassphraseHeading } from './PassphraseHeading'; interface PassphraseModalProps { device: TrezorDevice; @@ -57,12 +30,11 @@ export const PassphraseModal = ({ device }: PassphraseModalProps) => { const devices = useSelector(selectDevices); const authConfirmation = useSelector(selectIsDiscoveryAuthConfirmationRequired) || device.authConfirm; - + const deviceModel = useSelector(selectDeviceModel); 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 && @@ -83,108 +55,45 @@ export const PassphraseModal = ({ device }: PassphraseModalProps) => { [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 + const isPassphraseWalletConfirmationVisible = authConfirmation || stateConfirmation; + const isPassphraseWalletCreating = !hasEmptyPassphraseWallet && !stateConfirmation; + + // show borderless one-column modal for confirming passphrase and state confirmation + if (isPassphraseWalletConfirmationVisible) { return ( - - ) : ( - - ) - } - isCancelable + - ) : ( - - ) - } - > - } - offerPassphraseOnDevice={onDeviceOffer} - onSubmit={onSubmit} - singleColModal - learnMoreTooltipOnClick={ - - } - /> - + authConfirmation={authConfirmation} + onSubmit={onSubmit} + onDeviceOffer={onDeviceOffer} + device={device} + /> ); } // creating a hidden wallet - if (!noPassphraseOffer) { + if (isPassphraseWalletCreating) { 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} + deviceModel={deviceModel ?? undefined} learnMoreTooltipOnClick={ { /> } /> - - - - ); + + + ); + } + + // creating standard wallet here instead of showing dialog + onSubmit(''); + + return null; }; 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/ReduxModal/DeviceContextModal/PassphraseWalletConfirmation.tsx b/packages/suite/src/components/suite/modals/ReduxModal/DeviceContextModal/PassphraseWalletConfirmation.tsx new file mode 100644 index 000000000000..23206bf6a5ae --- /dev/null +++ b/packages/suite/src/components/suite/modals/ReduxModal/DeviceContextModal/PassphraseWalletConfirmation.tsx @@ -0,0 +1,192 @@ +import { TrezorDevice } from '@suite-common/suite-types'; +import { + Button, + Card, + Text, + PassphraseTypeCard, + Rows, + Columns, + Warning, + Icon, +} from '@trezor/components'; +import { useState } from 'react'; +import { OpenGuideFromTooltip } from 'src/components/guide'; +import { Translation } from 'src/components/suite/Translation'; +import { CardWithDevice } from 'src/views/suite/SwitchDevice/CardWithDevice'; +import { SwitchDeviceRenderer } from 'src/views/suite/SwitchDevice/SwitchDeviceRenderer'; +import { PassphraseList, PassphraseItem } from './PassphraseList'; +import { PassphraseHeading } from './PassphraseHeading'; +// import styled from 'styled-components'; + +interface PassphraseWalletConfirmationProps { + onCancel: () => void; + authConfirmation?: boolean; + onSubmit: (value: string, passphraseOnDevice?: boolean) => void; + onDeviceOffer: boolean; + device: TrezorDevice; +} + +// const Columns = styled.div` +// display: flex; +// flex-direction: row; +// justify-content: space-between; +// gap: ${spacingsPx.xs}; +// `; +// const Rows = styled.div` +// display: flex; +// flex-direction: column; +// gap: ${spacingsPx.sm}; +// `; + +type ContentType = 'step1' | 'step2' | 'step3'; + +export const PassphraseWalletConfirmation = ({ + onCancel, + authConfirmation, + onSubmit, + onDeviceOffer, + device, +}: PassphraseWalletConfirmationProps) => { + const [contentType, setContentType] = useState('step1'); + + const getContent = () => { + if (contentType === 'step1') { + return ( + <> + This passphrase wallet is empty + + + Learn how a passphrase works + + + } + > + + + Opening unused and knowingly empty passphrase wallet? + + + + + + + + + Expecting passphrase wallet with funds? + + + + + + + ); + } + + if (contentType === 'step2') + return ( + <> + What to do with new passphrase? + + + + + Write your passphrase on a piece of paper and always keep it offline + (no photos, USB, internet) + + + + + Store it in a different place than your backup seed + + + + Never share it with anyone, not even with Trezor support + + + + No one can recover it, not even Trezor support + + + ); + if (contentType === 'step3') + return ( + <> + Confirm passphrase + + Create an offline backup of your passphrase. It is irrecoverable, even by + Trezor support. + + } + offerPassphraseOnDevice={onDeviceOffer} + onSubmit={onSubmit} + singleColModal + learnMoreTooltipOnClick={ + + } + /> + + ); + }; + + return ( + + {/* + ) : ( + + ) + } + isCancelable + onCancel={onCancel} + onBackClick={authConfirmation ? onRecreate : undefined} + description={ + !authConfirmation ? ( + + ) : ( + + ) + } + > */} + + {getContent()} + + + ); +}; 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..c1e229094766 --- /dev/null +++ b/packages/suite/src/views/suite/SwitchDevice/CardWithDevice.tsx @@ -0,0 +1,83 @@ +import { ReactNode, useState } from 'react'; + +import { AnimatePresence, motion } from 'framer-motion'; +import styled from 'styled-components'; +import { Card, motionAnimation, useElevation } from '@trezor/components'; +import * as deviceUtils from '@suite-common/suite-utils'; + +import type { TrezorDevice, ForegroundAppProps } from 'src/types/suite'; +import { Elevation, mapElevationToBorder, spacingsPx } from '@trezor/theme'; + +import { DeviceHeader } from './DeviceItem/DeviceHeader'; + +const Content = styled.div<{ $elevation: Elevation }>` + padding-top: ${spacingsPx.xs}; + position: relative; + + &::before { + height: 1px; + content: ''; + background-color: ${({ $elevation, theme }) => mapElevationToBorder({ $elevation, theme })}; + position: absolute; + left: -12px; + right: -12px; + top: 0; + } +`; + +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'; + const { elevation } = useElevation(); + + 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..ec841fc20e79 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; @@ -17,6 +18,19 @@ const StyledTooltip = styled(Tooltip)` width: 100%; `; +const Rows = styled.div` + display: flex; + flex-direction: row; + gap: ${spacingsPx.xs}; +`; + +const Columns = styled.div` + display: flex; + flex: 1; + flex-direction: column; + gap: ${spacingsPx.xs}; +`; + interface AddWalletButtonProps { device: TrezorDevice; instances: AcquiredDevice[]; @@ -28,6 +42,8 @@ export const AddWalletButton = ({ device, instances, addDeviceInstance, + // addStandardWallet, + // addPassphraseWallet, selectDeviceInstance, }: AddWalletButtonProps) => { const hasAtLeastOneWallet = instances.find(d => d.state); @@ -44,7 +60,8 @@ export const AddWalletButton = ({ locks.includes(SUITE.LOCK_TYPE.DEVICE) || locks.includes(SUITE.LOCK_TYPE.UI); - const onAddWallet = () => { + // @TODO fix buttons + const onAddWallet = (isPassphraseWallet: boolean) => { if (hasAtLeastOneWallet) { addDeviceInstance(device); } else { @@ -59,24 +76,34 @@ export const AddWalletButton = ({ cursor="pointer" placement="bottom" > - )} - + + + ); 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..e118ca8aa442 --- /dev/null +++ b/packages/suite/src/views/suite/SwitchDevice/DeviceItem/DeviceHeader.tsx @@ -0,0 +1,77 @@ +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'; + +const Flex = styled.div` + flex: 1; +`; +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; + + 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..be8246b66142 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 () => { @@ -122,6 +74,14 @@ export const DeviceItem = ({ device, instances, onCancel, backgroundRoute }: Dev await dispatch(createDeviceInstance({ device: instance })); handleRedirection(); }; + // const addStandardWallet = async (instance: DeviceItemProps['device']) => { + // await dispatch(createDeviceInstance({ device: instance, useEmptyPassphrase: true })); + // handleRedirection(); + // }; + // const addPassphraseWallet = async (instance: DeviceItemProps['device']) => { + // await dispatch(createDeviceInstance({ device: instance, useEmptyPassphrase: false })); + // handleRedirection(); + // }; const onSolveIssueClick = () => { const needsAcquire = @@ -136,70 +96,40 @@ 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..b5083ee708cc --- /dev/null +++ b/packages/suite/src/views/suite/SwitchDevice/DeviceItem/EjectButton.tsx @@ -0,0 +1,42 @@ +import styled, { useTheme } from 'styled-components'; + +import { Tooltip, Icon } from '@trezor/components'; +import { Translation } from 'src/components/suite'; +import { borders, spacingsPx } from '@trezor/theme'; +import { ContentType } from '../types'; + +const EjectContainer = styled.div` + position: absolute; + right: ${spacingsPx.xs}; + top: ${spacingsPx.xs}; + background-color: white; + border-radius: ${borders.radii.full}; + padding: ${spacingsPx.xxs}; +`; + +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..5fcfd7a4f3bd 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,22 @@ export const ViewOnlyRadio = ({ }; export const ViewOnlyRadios = ({ isViewOnlyActive, - setIsViewOnlyActive, + toggleViewOnly, dataTest, }: ViewOnlyRadiosProps) => { - const [isViewOnlyActiveTemp, setIsViewOnlyActiveTemp] = useState(isViewOnlyActive); - const handleConfirm = () => { - setIsViewOnlyActive(isViewOnlyActiveTemp); + 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' && ( { @@ -38,37 +33,19 @@ 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} + + + ); };