From e581fe3cc1b354cefb8abb5f80dbf62f4e20c5fe Mon Sep 17 00:00:00 2001 From: mroz Date: Wed, 17 Aug 2022 14:35:04 +0200 Subject: [PATCH] feat(connect-popup): passphrase redesign [skip ci] --- .../Passphrase/PassphraseTypeCard.tsx} | 68 ++++++---- .../Passphrase/PasswordStrengthIndicator.tsx} | 0 packages/components/src/index.ts | 1 + packages/connect-popup/src/index.tsx | 22 +++- packages/connect-popup/src/view/index.ts | 1 - packages/connect-popup/src/view/passphrase.ts | 118 ------------------ packages/connect-popup/tsconfig.json | 1 + packages/connect-ui/src/index.ts | 2 + packages/connect-ui/src/views/Passphrase.tsx | 69 ++++++++++ .../src/views/PassphraseOnDevice.tsx | 13 ++ .../suite/modals/Passphrase/index.tsx | 4 +- .../src/components/suite/modals/index.tsx | 1 + packages/suite/src/hooks/suite/index.ts | 2 + packages/suite/src/hooks/suite/useKeyPress.ts | 37 ++++++ .../src/hooks/suite/useOnClickOutside.ts | 31 +++++ 15 files changed, 221 insertions(+), 149 deletions(-) rename packages/{suite/src/components/suite/modals/Passphrase/components/PassphraseTypeCard/index.tsx => components/src/components/Passphrase/PassphraseTypeCard.tsx} (86%) rename packages/{suite/src/components/suite/PasswordStrengthIndicator/index.tsx => components/src/components/Passphrase/PasswordStrengthIndicator.tsx} (100%) delete mode 100644 packages/connect-popup/src/view/passphrase.ts create mode 100644 packages/connect-ui/src/views/Passphrase.tsx create mode 100644 packages/connect-ui/src/views/PassphraseOnDevice.tsx create mode 100644 packages/suite/src/hooks/suite/useKeyPress.ts create mode 100644 packages/suite/src/hooks/suite/useOnClickOutside.ts diff --git a/packages/suite/src/components/suite/modals/Passphrase/components/PassphraseTypeCard/index.tsx b/packages/components/src/components/Passphrase/PassphraseTypeCard.tsx similarity index 86% rename from packages/suite/src/components/suite/modals/Passphrase/components/PassphraseTypeCard/index.tsx rename to packages/components/src/components/Passphrase/PassphraseTypeCard.tsx index fa58d4679a3d..a31456c8805d 100644 --- a/packages/suite/src/components/suite/modals/Passphrase/components/PassphraseTypeCard/index.tsx +++ b/packages/components/src/components/Passphrase/PassphraseTypeCard.tsx @@ -1,17 +1,33 @@ +// pull this component up to some share logic. + +// todo: reorganize imports + import React, { useState, useRef, useEffect, useCallback } from 'react'; import { AnimatePresence, motion } from 'framer-motion'; -import { useKeyPress } from '@trezor/react-utils'; -import { ANIMATION } from '@suite-config'; -import { setCaretPosition } from '@suite-utils/dom'; import styled, { css } from 'styled-components'; + +// ok import { Button, useTheme, variables, Input, Tooltip, Checkbox, Icon } from '@trezor/components'; -import { Translation } from '@suite-components/Translation'; -import { MAX_LENGTH } from '@suite-constants/inputs'; import { countBytesInString } from '@trezor/utils'; -import { OpenGuideFromTooltip } from '@guide-components'; -import PasswordStrengthIndicator from '@suite-components/PasswordStrengthIndicator'; -import { useTranslation } from '@suite-hooks'; -import { isAndroid } from '@suite-utils/env'; +import { useKeyPress } from '@trezor/react-utils'; + +// move to another package, shared package should not import from suite +import { setCaretPosition } from '@trezor/suite/src/utils/suite/dom'; +import ANIMATION from '@trezor/suite/src/config/suite/animation'; + +// moved +import PasswordStrengthIndicator from './PasswordStrengthIndicator'; + +// import { MAX_LENGTH } from '@suite-constants/inputs'; +const MAX_LENGTH = { + PASSPHRASE: 50, +}; // probably should become PROP or be imported from some @trezor/constants/common package + +// todo: refactor these, translations, modal, etc +// import { useTranslation } from '@suite-hooks'; +// import { OpenGuideFromTooltip } from '@guide-components'; +// import { Translation } from '@suite-components/Translation'; +// import { isAndroid } from '@trezor/suite/src/utils/suite/env'; const Wrapper = styled.div>` display: flex; @@ -151,6 +167,9 @@ const RetryButton = styled(Button)` margin-top: 16px; `; +// todo: how about translations? pass translation component? pass translated string? +const Translation = ({ id }: { id: string }) =>
{id}
; + type Props = { title?: React.ReactNode; description?: React.ReactNode; @@ -165,9 +184,9 @@ type Props = { const DOT = '●'; -const PassphraseTypeCard = (props: Props) => { +export const PassphraseTypeCard = (props: Props) => { const theme = useTheme(); - const { translationString } = useTranslation(); + // const { translationString } = useTranslation(); const [value, setValue] = useState(''); const [enabled, setEnabled] = useState(!props.authConfirmation); const [showPassword, setShowPassword] = useState(false); @@ -284,15 +303,16 @@ const PassphraseTypeCard = (props: Props) => { > {props.type === 'hidden' ? ( } - guideAnchor={instance => ( - - )} - content={} + // title={} + // guideAnchor={instance => ( + // + // )} + // content={} + title={
"todo: pass tooltip as prop?"
} dashed > <>{props.title} @@ -319,7 +339,8 @@ const PassphraseTypeCard = (props: Props) => { { inputState={isTooLong ? 'error' : undefined} noTopLabel noError - autoFocus={!isAndroid()} + // autoFocus={!isAndroid()} + autoFocus innerAddon={ { ); }; - -export default PassphraseTypeCard; diff --git a/packages/suite/src/components/suite/PasswordStrengthIndicator/index.tsx b/packages/components/src/components/Passphrase/PasswordStrengthIndicator.tsx similarity index 100% rename from packages/suite/src/components/suite/PasswordStrengthIndicator/index.tsx rename to packages/components/src/components/Passphrase/PasswordStrengthIndicator.tsx diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index dcc5acef3aee..53fc44a3b227 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -44,6 +44,7 @@ export * from './components/form/SelectBar'; export * from './components/HoverAnimation'; export * from './components/Fade'; export * from './components/Image/Image'; +export * from './components/Passphrase/PassphraseTypeCard'; export * from './constants/keyboardEvents'; diff --git a/packages/connect-popup/src/index.tsx b/packages/connect-popup/src/index.tsx index 4c57167beb4f..9338b6e886ee 100644 --- a/packages/connect-popup/src/index.tsx +++ b/packages/connect-popup/src/index.tsx @@ -9,8 +9,10 @@ import { PopupEvent, PopupInit, PopupHandshake, + UI, + createUiResponse, } from '@trezor/connect'; -import { Transport } from '@trezor/connect-ui'; +import { Transport, Passphrase, PassphraseOnDevice } from '@trezor/connect-ui'; import * as view from './view'; import { @@ -149,11 +151,25 @@ const handleMessage = (event: MessageEvent) => { break; // comes first case UI_REQUEST.REQUEST_PASSPHRASE: - view.initPassphraseView(message.payload); + showView( + { + postMessage( + createUiResponse(UI.RECEIVE_PASSPHRASE, { + value, + passphraseOnDevice, + // todo: what is this param? + save: true, + }), + ); + }} + />, + ); break; // comes when user clicks "enter on device" case UI_REQUEST.REQUEST_PASSPHRASE_ON_DEVICE: - view.passphraseOnDeviceView(message.payload); + showView(); break; case UI_REQUEST.INVALID_PASSPHRASE: view.initInvalidPassphraseView(message.payload); diff --git a/packages/connect-popup/src/view/index.ts b/packages/connect-popup/src/view/index.ts index b2e25425efb9..f1fae98ea960 100644 --- a/packages/connect-popup/src/view/index.ts +++ b/packages/connect-popup/src/view/index.ts @@ -2,7 +2,6 @@ export { showView, postMessage } from './common'; export { initPinView } from './pin'; -export { initPassphraseView } from './passphrase'; export { initInvalidPassphraseView } from './invalidPassphrase'; export { initWordView } from './word'; export { selectDevice } from './selectDevice'; diff --git a/packages/connect-popup/src/view/passphrase.ts b/packages/connect-popup/src/view/passphrase.ts deleted file mode 100644 index 8e0155720e44..000000000000 --- a/packages/connect-popup/src/view/passphrase.ts +++ /dev/null @@ -1,118 +0,0 @@ -// origin: https://github.com/trezor/connect/blob/develop/src/js/popup/view/passphrase.js - -import { UI, createUiResponse, UiRequestDeviceAction } from '@trezor/connect'; -import { container, showView, postMessage } from './common'; - -export const initPassphraseView = (payload: UiRequestDeviceAction['payload']) => { - showView('passphrase'); - - const view = container.getElementsByClassName('passphrase')[0]; - const deviceNameSpan = container.getElementsByClassName('device-name')[0] as HTMLElement; - const input1 = container.getElementsByClassName('pass')[0] as HTMLInputElement; - const input2 = container.getElementsByClassName('pass-check')[0] as HTMLInputElement; - const toggle = container.getElementsByClassName('show-passphrase')[0] as HTMLInputElement; - const enter = container.getElementsByClassName('submit')[0] as HTMLButtonElement; - - let inputType = 'password'; - - const { label, features } = payload.device; - deviceNameSpan.innerText = label; - const passphraseOnDevice = - features && - features.capabilities && - features.capabilities.includes('Capability_PassphraseEntry'); - - /* Functions */ - const validation = () => { - if (input1.value !== input2.value) { - enter.disabled = true; - view.classList.add('not-valid'); - } else { - enter.disabled = false; - view.classList.remove('not-valid'); - } - }; - const toggleInputFontStyle = (input: HTMLInputElement) => { - if (inputType === 'text') { - // input.classList.add('text'); - input.setAttribute('type', 'text'); - - // Since passphrase is visible there's no need to force user to fill the passphrase twice - // - disable input2 - // - write automatically into input2 as the user is writing into input1 (listen to input event) - input2.disabled = true; - input2.value = input1.value; - validation(); - } else if (inputType === 'password') { - // input.classList.remove('text'); - input.setAttribute('type', 'password'); - - input2.disabled = false; - input2.value = ''; - validation(); - } - }; - const handleToggleClick = () => { - inputType = inputType === 'text' ? 'password' : 'text'; - - toggleInputFontStyle(input1); - toggleInputFontStyle(input2); - }; - const handleWindowKeydown = (e: KeyboardEvent) => { - if (e.key === 'Enter') { - e.preventDefault(); - enter.click(); - } - }; - const handleEnterClick = () => { - input1.blur(); - input2.blur(); - window.removeEventListener('keydown', handleWindowKeydown); - - showView('loader'); - postMessage( - createUiResponse(UI.RECEIVE_PASSPHRASE, { - value: input1.value, - save: true, - }), - ); - }; - - /* Functions: END */ - input1.addEventListener( - 'input', - () => { - validation(); - if (inputType === 'text') { - input2.value = input1.value; - validation(); - } - }, - false, - ); - input2.addEventListener('input', validation, false); - - toggle.addEventListener('click', handleToggleClick); - enter.addEventListener('click', handleEnterClick); - window.addEventListener('keydown', handleWindowKeydown, false); - - if (passphraseOnDevice) { - const onDevice = container.getElementsByClassName( - 'passphraseOnDevice', - )[0] as HTMLButtonElement; - onDevice.style.display = 'block'; - onDevice.addEventListener('click', () => { - window.removeEventListener('keydown', handleWindowKeydown); - showView('loader'); - postMessage( - createUiResponse(UI.RECEIVE_PASSPHRASE, { - value: '', - passphraseOnDevice: true, - save: true, - }), - ); - }); - } - - input1.focus(); -}; diff --git a/packages/connect-popup/tsconfig.json b/packages/connect-popup/tsconfig.json index 363aabd4a57d..1531457aed81 100644 --- a/packages/connect-popup/tsconfig.json +++ b/packages/connect-popup/tsconfig.json @@ -5,6 +5,7 @@ "types": ["web"] }, "references": [ + { "path": "../components" }, { "path": "../connect" }, { "path": "../components" }, { "path": "../connect-ui" }, diff --git a/packages/connect-ui/src/index.ts b/packages/connect-ui/src/index.ts index a99b78435673..a7f92c3d4f21 100644 --- a/packages/connect-ui/src/index.ts +++ b/packages/connect-ui/src/index.ts @@ -1,4 +1,6 @@ export * from './views/Transport'; +export * from './views/Passphrase'; +export * from './views/PassphraseOnDevice'; // this export will be removed in the future but it is required now // future => connect-ui will have its own entrypoint diff --git a/packages/connect-ui/src/views/Passphrase.tsx b/packages/connect-ui/src/views/Passphrase.tsx new file mode 100644 index 000000000000..38c438e8ae23 --- /dev/null +++ b/packages/connect-ui/src/views/Passphrase.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import styled from 'styled-components'; + +import { variables, PassphraseTypeCard } from '@trezor/components'; + +import { View } from '../components/View'; + +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 16px; + height: 1px; + background: ${props => props.theme.STROKE_GREY}; +`; + +// todo: +const Translation = (props: any) => {props.id}; + +// todo: +export const Passphrase = (props: any) => { + console.log('Passphrase.props', props); + const { device, onPassphraseSubmit } = props; + const { features } = device; + + const offerPassphraseOnDevice = + features && + features.capabilities && + features.capabilities.includes('Capability_PassphraseEntry'); + + return ( + + {/* todo: this part could be shared with suite? */} + + + } + description={} + submitLabel={} + type="standard" + onSubmit={onPassphraseSubmit} + /> + + } + description={} + submitLabel={} + type="hidden" + offerPassphraseOnDevice={offerPassphraseOnDevice} + onSubmit={onPassphraseSubmit} + /> + + + + ); +}; diff --git a/packages/connect-ui/src/views/PassphraseOnDevice.tsx b/packages/connect-ui/src/views/PassphraseOnDevice.tsx new file mode 100644 index 000000000000..6692ec87e6d3 --- /dev/null +++ b/packages/connect-ui/src/views/PassphraseOnDevice.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +// todo: maybe follow instructions on device would be enough? +// todo: +export const PassphraseOnDevice = (props: any) => { + console.log('Passphrase.props', props); + + return ( + <> +

Passphrase on device

+ + ); +}; diff --git a/packages/suite/src/components/suite/modals/Passphrase/index.tsx b/packages/suite/src/components/suite/modals/Passphrase/index.tsx index aa0f561cdf2a..a66b78c0ae67 100644 --- a/packages/suite/src/components/suite/modals/Passphrase/index.tsx +++ b/packages/suite/src/components/suite/modals/Passphrase/index.tsx @@ -1,20 +1,18 @@ import TrezorConnect from '@trezor/connect'; import React, { useState } from 'react'; import styled from 'styled-components'; -import { variables } from '@trezor/components'; +import { variables, PassphraseTypeCard } from '@trezor/components'; import { useSelector, useActions } from '@suite-hooks'; import * as modalActions from '@suite-actions/modalActions'; import * as discoveryActions from '@wallet-actions/discoveryActions'; import * as deviceUtils from '@suite-utils/device'; import { Translation, Modal } from '@suite-components'; -import PassphraseTypeCard from './components/PassphraseTypeCard'; import type { TrezorDevice } from '@suite-types'; const Wrapper = styled.div<{ authConfirmation?: boolean }>` display: flex; flex-direction: column; align-items: center; - /* width: ${props => (props.authConfirmation ? 'auto' : '660px')}; */ @media screen and (max-width: ${variables.SCREEN_SIZE.MD}) { width: 100%; diff --git a/packages/suite/src/components/suite/modals/index.tsx b/packages/suite/src/components/suite/modals/index.tsx index 04d89bd937b4..ef2e48ce702b 100644 --- a/packages/suite/src/components/suite/modals/index.tsx +++ b/packages/suite/src/components/suite/modals/index.tsx @@ -1,6 +1,7 @@ export { Pin } from './Pin'; export { PinInvalid } from './PinInvalid'; export { PinMismatch } from './PinMismatch'; +// todo: sharing with connect-popup export { Passphrase } from './Passphrase'; export { PassphraseSource } from './PassphraseSource'; export { PassphraseOnDevice } from './PassphraseOnDevice'; diff --git a/packages/suite/src/hooks/suite/index.ts b/packages/suite/src/hooks/suite/index.ts index 3307c0401c25..366ab6383fef 100644 --- a/packages/suite/src/hooks/suite/index.ts +++ b/packages/suite/src/hooks/suite/index.ts @@ -14,6 +14,8 @@ export { useRecovery } from './useRecovery'; export { useExternalLink } from './useExternalLink'; export { useFilteredModal } from './useFilteredModal'; export { usePreferredModal } from './usePreferredModal'; +export { useKeyPress } from './useKeyPress'; +export { useOnClickOutside } from './useOnClickOutside'; // replaced in suite-native export { useLocales } from '@suite-hooks/useLocales'; diff --git a/packages/suite/src/hooks/suite/useKeyPress.ts b/packages/suite/src/hooks/suite/useKeyPress.ts new file mode 100644 index 000000000000..edf944ed55b0 --- /dev/null +++ b/packages/suite/src/hooks/suite/useKeyPress.ts @@ -0,0 +1,37 @@ +import { useState, useEffect } from 'react'; + +// todo: duplicity +export const useKeyPress = (targetKey: string) => { + // State for keeping track of whether key is pressed + const [keyPressed, setKeyPressed] = useState(false); + + // If pressed key is our target key then set to true + // eslint-disable-next-line react-hooks/exhaustive-deps + const downHandler = (event: KeyboardEvent) => { + if (event.key === targetKey) { + setKeyPressed(true); + } + }; + + // If released key is our target key then set to false + // eslint-disable-next-line react-hooks/exhaustive-deps + const upHandler = (event: KeyboardEvent) => { + if (event.key === targetKey) { + setKeyPressed(false); + } + }; + + // Add event listeners + useEffect(() => { + window.addEventListener('keydown', downHandler); + window.addEventListener('keyup', upHandler); + // Remove event listeners on cleanup + return () => { + window.removeEventListener('keydown', downHandler); + window.removeEventListener('keyup', upHandler); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); // Empty array ensures that effect is only run on mount and unmount + + return keyPressed; +}; diff --git a/packages/suite/src/hooks/suite/useOnClickOutside.ts b/packages/suite/src/hooks/suite/useOnClickOutside.ts new file mode 100644 index 000000000000..7e2a7da3ffdd --- /dev/null +++ b/packages/suite/src/hooks/suite/useOnClickOutside.ts @@ -0,0 +1,31 @@ +import { useEffect } from 'react'; + +export const useOnClickOutside = ( + elementRefs: React.MutableRefObject[], + callback: (event: MouseEvent | TouchEvent) => void, +) => { + useEffect(() => { + if (!elementRefs?.length) return; + const listener = (event: MouseEvent | TouchEvent) => { + let clickInsideElements = false; + + elementRefs.forEach(elRef => { + // Do nothing if clicking ref's element or descendent elements + if (!elRef.current || elRef.current.contains(event.target as Node)) { + clickInsideElements = true; + } + }); + if (clickInsideElements) return; + + callback(event); + }; + + document.addEventListener('mousedown', listener); + document.addEventListener('touchstart', listener); + + return () => { + document.removeEventListener('mousedown', listener); + document.removeEventListener('touchstart', listener); + }; + }, [elementRefs, callback]); +};