From 108fdf865e00b545f29f8fca15e7a7869a8da097 Mon Sep 17 00:00:00 2001 From: kyranjamie Date: Wed, 9 Aug 2023 18:56:20 +0200 Subject: [PATCH] refactor: improve secret key ux --- src/app/common/utils.ts | 7 - .../onboarding/sign-in/hooks/use-sign-in.ts | 11 +- src/app/pages/onboarding/sign-in/sign-in.tsx | 253 +++++++++++------- src/shared/route-urls.ts | 1 - tests-legacy/page-objects/wallet.page.ts | 11 +- tests-legacy/page-objects/wallet.selectors.ts | 1 - tests/page-object-models/onboarding.page.ts | 7 +- 7 files changed, 169 insertions(+), 122 deletions(-) diff --git a/src/app/common/utils.ts b/src/app/common/utils.ts index b5e67bd371..e20c1f6cf3 100644 --- a/src/app/common/utils.ts +++ b/src/app/common/utils.ts @@ -1,5 +1,3 @@ -import type { ClipboardEvent } from 'react'; - import { hexToBytes } from '@stacks/common'; import { BytesReader, @@ -42,11 +40,6 @@ export function extractPhraseFromString(value: string) { } } -export function extractPhraseFromPasteEvent(event: ClipboardEvent) { - const pasted = event.clipboardData.getData('Text'); - return extractPhraseFromString(pasted); -} - interface MakeTxExplorerLinkArgs { blockchain: Blockchains; mode: BitcoinNetworkModes; diff --git a/src/app/pages/onboarding/sign-in/hooks/use-sign-in.ts b/src/app/pages/onboarding/sign-in/hooks/use-sign-in.ts index 6c77056dba..eb08fb3207 100644 --- a/src/app/pages/onboarding/sign-in/hooks/use-sign-in.ts +++ b/src/app/pages/onboarding/sign-in/hooks/use-sign-in.ts @@ -7,7 +7,7 @@ import { RouteUrls } from '@shared/route-urls'; import { useAnalytics } from '@app/common/hooks/analytics/use-analytics'; import { useLoading } from '@app/common/hooks/use-loading'; -import { delay, extractPhraseFromPasteEvent } from '@app/common/utils'; +import { delay } from '@app/common/utils'; import { useAppDispatch } from '@app/store'; import { inMemoryKeyActions } from '@app/store/in-memory-key/in-memory-key.actions'; import { onboardingActions } from '@app/store/onboarding/onboarding.actions'; @@ -67,14 +67,6 @@ export function useSignIn() { [setIsLoading, dispatch, analytics, navigate, setIsIdle, handleSetError] ); - const onPaste = useCallback( - async (event: React.ClipboardEvent) => { - const value = extractPhraseFromPasteEvent(event); - await submitMnemonicForm(value); - }, - [submitMnemonicForm] - ); - const toggleKeyMask = useCallback(() => { setIsKeyMasked(prev => !prev); }, []); @@ -90,7 +82,6 @@ export function useSignIn() { ); return { - onPaste, submitMnemonicForm, ref: textAreaRef, error, diff --git a/src/app/pages/onboarding/sign-in/sign-in.tsx b/src/app/pages/onboarding/sign-in/sign-in.tsx index 65c861f89f..195276a829 100644 --- a/src/app/pages/onboarding/sign-in/sign-in.tsx +++ b/src/app/pages/onboarding/sign-in/sign-in.tsx @@ -1,124 +1,187 @@ -import { FiEye, FiEyeOff } from 'react-icons/fi'; +import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import YourSecretKey from '@assets/images/onboarding/your-secret-key.png'; -import { css } from '@emotion/react'; -import { Box, Input, Stack, Text, color, useMediaQuery } from '@stacks/ui'; +import { Box, Button, Flex, Grid, Input, Stack, Text, color, useMediaQuery } from '@stacks/ui'; import { OnboardingSelectors } from '@tests/selectors/onboarding.selectors'; -import { Form, Formik } from 'formik'; +import { useFocus } from 'use-events'; import { RouteUrls } from '@shared/route-urls'; import { useRouteHeader } from '@app/common/hooks/use-route-header'; +import { createNullArrayOfLength, extractPhraseFromString } from '@app/common/utils'; import { CenteredPageContainer } from '@app/components/centered-page-container'; import { ErrorLabel } from '@app/components/error-label'; -import { - CENTERED_FULL_PAGE_MAX_WIDTH, - DESKTOP_VIEWPORT_MIN_WIDTH, -} from '@app/components/global-styles/full-page-styles'; +import { DESKTOP_VIEWPORT_MIN_WIDTH } from '@app/components/global-styles/full-page-styles'; import { Header } from '@app/components/header'; -import { Link } from '@app/components/link'; import { PageTitle } from '@app/components/page-title'; import { PrimaryButton } from '@app/components/primary-button'; -import { Title } from '@app/components/typography'; +import { Caption, Title } from '@app/components/typography'; import { useSignIn } from '@app/pages/onboarding/sign-in/hooks/use-sign-in'; +interface MnemonicWordInputProps { + index: number; + value: string; + onUpdateWord(word: string): void; + onPasteEntireKey(word: string): void; +} +function MnemonicWordInput({ + index, + value, + onUpdateWord, + onPasteEntireKey, +}: MnemonicWordInputProps) { + const [isFocused, bind] = useFocus(); + + return ( + + { + const pasteValue = extractPhraseFromString(e.clipboardData.getData('text')); + if (pasteValue.includes(' ')) { + e.preventDefault(); + //assume its a full key + onPasteEntireKey(pasteValue); + } + }} + onChange={(e: any) => { + e.preventDefault(); + onUpdateWord(e.target.value); + }} + {...bind} + /> + + ); +} + export function SignIn() { - const { onPaste, submitMnemonicForm, error, isLoading, ref, toggleKeyMask, isKeyMasked } = - useSignIn(); + const { submitMnemonicForm, error, isLoading } = useSignIn(); const navigate = useNavigate(); + const [twentyFourWordMode, setTwentyFourWordMode] = useState(true); + const [desktopViewport] = useMediaQuery(`(min-width: ${DESKTOP_VIEWPORT_MIN_WIDTH})`); useRouteHeader(
navigate(RouteUrls.Onboarding)} hideActions />); + const [mnemonic, setMnemonic] = useState<(string | null)[]>(() => createNullArrayOfLength(24)); + + function mnemonicWordUpdate(index: number, word: string) { + const newMnemonic = [...mnemonic]; + newMnemonic[index] = word; + setMnemonic(newMnemonic); + } + + function updateEntireKey(key: string) { + const newKey = key.split(' '); + setMnemonic(newKey); + void submitMnemonicForm(key); + } + return ( - submitMnemonicForm(values.secretKey)} + { + e.preventDefault(); + void submitMnemonicForm(mnemonic.join(' ')); + }} + px={['loose', 'base-loose']} + spacing={['loose', 'extra-loose']} + textAlign={['left', 'center']} > - {form => ( -
- - - - - {desktopViewport ? ( - Sign in with your Secret Key - ) : ( - <> - Sign in with Secret Key - - Enter your 12- or 24-word Secret Key to sign in with an existing wallet - - - )} - - e.key === 'Enter' && form.submitForm()} - onPaste={onPaste} - placeholder="Paste or type your Secret Key" - ref={ref as any} - spellCheck={false} - style={{ resize: 'none' }} - width="100%" - {...form.getFieldProps('secretKey')} - /> - {error && ( - - - {error} - - - )} - - - - {isKeyMasked ? : } - {isKeyMasked ? 'Show' : 'Hide'} Secret Key - - - - - - Continue - - -
+ + + + {desktopViewport ? ( + Sign in with your Secret Key + ) : ( + <> + Sign in with Secret Key + )} -
+ + + Enter your Secret Key to sign in with an existing wallet + + Tip: You can paste in your entire Secret Key at once + + + + {createNullArrayOfLength(twentyFourWordMode ? 24 : 12).map((_, i) => ( + { + (document.activeElement as any)?.blur(); + updateEntireKey(key); + }} + onUpdateWord={w => mnemonicWordUpdate(i, w)} + /> + ))} + + + + {error && ( + + + {error} + + + )} + + Continue + + + +
); } diff --git a/src/shared/route-urls.ts b/src/shared/route-urls.ts index da61af372c..e751d83155 100644 --- a/src/shared/route-urls.ts +++ b/src/shared/route-urls.ts @@ -6,7 +6,6 @@ export enum RouteUrls { BackUpSecretKey = '/back-up-secret-key', SetPassword = '/set-password', SignIn = '/sign-in', - MagicRecoveryCode = '/recovery-code', RequestDiagnostics = '/request-diagnostics', // Ledger routes diff --git a/tests-legacy/page-objects/wallet.page.ts b/tests-legacy/page-objects/wallet.page.ts index 4900850424..b24fa9578b 100644 --- a/tests-legacy/page-objects/wallet.page.ts +++ b/tests-legacy/page-objects/wallet.page.ts @@ -51,7 +51,6 @@ export class WalletPage { $signOutDeleteWalletBtn = createTestSelector(SettingsSelectors.BtnSignOutActuallyDeleteWallet); $enterPasswordInput = createTestSelector(SettingsSelectors.EnterPasswordInput); $unlockWalletBtn = createTestSelector(SettingsSelectors.UnlockWalletBtn); - $magicRecoveryMessage = createTestSelector(WalletPageSelectors.MagicRecoveryMessage); $hideStepsBtn = createTestSelector(OnboardingSelectors.HideStepsBtn); $suggestedStepsList = createTestSelector(OnboardingSelectors.StepsList); $suggestedStepStartBtn = createTestSelector(OnboardingSelectors.StepItemStart); @@ -149,8 +148,10 @@ export class WalletPage { } async enterSecretKey(secretKey: string) { - await this.page.waitForSelector('textarea'); - await this.page.fill('textarea', secretKey); + const key = secretKey.split(' '); + for (let i = 0; i < key.length; i++) { + await this.page.getByTestId(`mnemonic-input-${i}`).fill(key[i]); + } await this.page.click(this.$buttonSignInKeyContinue); } @@ -205,10 +206,6 @@ export class WalletPage { await this.page.click(this.$fundAccountBtn); } - async waitForMagicRecoveryMessage() { - await this.page.waitForSelector(this.$magicRecoveryMessage, { timeout: 30000 }); - } - async waitForSendButton() { await this.page.waitForSelector(this.$sendTokenBtn, { timeout: 30000 }); } diff --git a/tests-legacy/page-objects/wallet.selectors.ts b/tests-legacy/page-objects/wallet.selectors.ts index 220069ad2f..03fa378cbc 100644 --- a/tests-legacy/page-objects/wallet.selectors.ts +++ b/tests-legacy/page-objects/wallet.selectors.ts @@ -1,4 +1,3 @@ export enum WalletPageSelectors { - MagicRecoveryMessage = 'magic-recovery-message', StatusMessage = 'status-message', } diff --git a/tests/page-object-models/onboarding.page.ts b/tests/page-object-models/onboarding.page.ts index d8a0fe0161..a4b7849218 100644 --- a/tests/page-object-models/onboarding.page.ts +++ b/tests/page-object-models/onboarding.page.ts @@ -127,7 +127,12 @@ export class OnboardingPage { async signInExistingUser() { await this.denyAnalytics(); await this.page.getByTestId(OnboardingSelectors.SignInLink).click(); - await this.page.getByTestId(OnboardingSelectors.SecretKeyInput).fill(TEST_SECRET_KEY); + + const key = TEST_SECRET_KEY.split(' '); + for (let i = 0; i < key.length; i++) { + await this.page.getByTestId(`mnemonic-input-${i}`).fill(key[i]); + } + await this.page.getByTestId(OnboardingSelectors.SignInBtn).click(); await this.setPassword(); await this.page.waitForURL('**' + RouteUrls.Home);