Skip to content

Commit

Permalink
refactor: improve secret key ux
Browse files Browse the repository at this point in the history
  • Loading branch information
kyranjamie committed Aug 10, 2023
1 parent 5f61753 commit 108fdf8
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 122 deletions.
7 changes: 0 additions & 7 deletions src/app/common/utils.ts
@@ -1,5 +1,3 @@
import type { ClipboardEvent } from 'react';

import { hexToBytes } from '@stacks/common';
import {
BytesReader,
Expand Down Expand Up @@ -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;
Expand Down
11 changes: 1 addition & 10 deletions src/app/pages/onboarding/sign-in/hooks/use-sign-in.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
}, []);
Expand All @@ -90,7 +82,6 @@ export function useSignIn() {
);

return {
onPaste,
submitMnemonicForm,
ref: textAreaRef,
error,
Expand Down
253 changes: 158 additions & 95 deletions 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 (
<Box
position="relative"
_after={{
content: `"${index + 1}."`,
textAlign: 'right',
position: 'absolute',
top: 0,
left: '-22px',
lineHeight: '48px',
color: color('text-caption'),
fontSize: '12px',
width: '18px',
}}
>
<Input
type={isFocused ? 'text' : 'password'}
value={value}
autoCapitalize="off"
spellCheck={false}
autoComplete="off"
data-testid={`mnemonic-input-${index}`}
onPaste={e => {
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}
/>
</Box>
);
}

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(<Header onClose={() => 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 (
<CenteredPageContainer>
<Formik
initialValues={{ secretKey: '' }}
onSubmit={values => submitMnemonicForm(values.secretKey)}
<Stack
as="form"
onSubmit={e => {
e.preventDefault();
void submitMnemonicForm(mnemonic.join(' '));
}}
px={['loose', 'base-loose']}
spacing={['loose', 'extra-loose']}
textAlign={['left', 'center']}
>
{form => (
<Form>
<Stack
maxWidth={CENTERED_FULL_PAGE_MAX_WIDTH}
px={['loose', 'base-loose']}
spacing={['loose', 'extra-loose']}
textAlign={['left', 'center']}
>
<Box alignSelf={['start', 'center']} width={['81px', '101px']}>
<img src={YourSecretKey} />
</Box>
{desktopViewport ? (
<PageTitle>Sign in with your Secret Key</PageTitle>
) : (
<>
<Title as="h1">Sign in with Secret Key</Title>
<Text color={color('text-caption')}>
Enter your 12- or 24-word Secret Key to sign in with an existing wallet
</Text>
</>
)}
<Stack spacing="base-tight">
<Input
data-testid={OnboardingSelectors.SecretKeyInput}
css={
isKeyMasked &&
css`
color: transparent;
caret-color: ${color('text-body')};
${form.values.secretKey && 'text-shadow: 0 0 8px rgba(0, 0, 0, 0.8)'};
`
}
as="textarea"
autoCapitalize="off"
autoFocus
borderRadius="10px"
fontSize="16px"
minHeight="168px"
onKeyDown={e => 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 && (
<ErrorLabel>
<Text
data-testid="sign-in-seed-error"
color={color('feedback-error')}
maxWidth="340px"
pr="extra-loose"
textAlign="left"
textStyle="caption"
>
{error}
</Text>
</ErrorLabel>
)}
<Stack alignItems="center">
<Link fontSize="14px" _hover={{ textDecoration: 'none' }} onClick={toggleKeyMask}>
<Stack alignItems="center" isInline spacing="tight">
{isKeyMasked ? <FiEye /> : <FiEyeOff />}
<Text>{isKeyMasked ? 'Show' : 'Hide'} Secret Key</Text>
</Stack>
</Link>
</Stack>
</Stack>
<PrimaryButton
data-testid={OnboardingSelectors.SignInBtn}
isDisabled={isLoading}
isLoading={isLoading}
>
Continue
</PrimaryButton>
</Stack>
</Form>
<Box alignSelf={['start', 'center']} width={['81px', '101px']}>
<img src={YourSecretKey} />
</Box>
{desktopViewport ? (
<PageTitle>Sign in with your Secret Key</PageTitle>
) : (
<>
<Title as="h1">Sign in with Secret Key</Title>
</>
)}
</Formik>
<Box>
<Text color={color('text-caption')}>
Enter your Secret Key to sign in with an existing wallet
</Text>
<Caption mt="base-tight">Tip: You can paste in your entire Secret Key at once</Caption>
</Box>
<Stack spacing="base-tight">
<Grid
mx="base"
templateColumns={['repeat(2, minmax(30%, 1fr))', 'repeat(3, minmax(120px, 1fr))']}
rowGap="30px"
columnGap="30px"
>
{createNullArrayOfLength(twentyFourWordMode ? 24 : 12).map((_, i) => (
<MnemonicWordInput
index={i}
key={i}
value={mnemonic[i] ?? ''}
onPasteEntireKey={key => {
(document.activeElement as any)?.blur();
updateEntireKey(key);
}}
onUpdateWord={w => mnemonicWordUpdate(i, w)}
/>
))}
</Grid>
</Stack>
<Flex flexDirection="column" justifyContent="center" alignItems="center">
{error && (
<ErrorLabel mb="loose" alignItems="center">
<Text
data-testid="sign-in-seed-error"
color={color('feedback-error')}
pr="extra-loose"
textStyle="caption"
>
{error}
</Text>
</ErrorLabel>
)}
<PrimaryButton
data-testid={OnboardingSelectors.SignInBtn}
isDisabled={isLoading}
isLoading={isLoading}
width="320px"
>
Continue
</PrimaryButton>
<Button
mt="loose"
variant="link"
textStyle="caption"
color={color('text-caption')}
type="button"
onClick={() => {
setTwentyFourWordMode(!twentyFourWordMode);
setMnemonic(createNullArrayOfLength(twentyFourWordMode ? 24 : 12));
}}
>
{twentyFourWordMode ? 'Have a 12-word Secret Key?' : 'Use 24 word Secret Key'}
</Button>
</Flex>
</Stack>
</CenteredPageContainer>
);
}
1 change: 0 additions & 1 deletion src/shared/route-urls.ts
Expand Up @@ -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
Expand Down
11 changes: 4 additions & 7 deletions tests-legacy/page-objects/wallet.page.ts
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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 });
}
Expand Down
1 change: 0 additions & 1 deletion tests-legacy/page-objects/wallet.selectors.ts
@@ -1,4 +1,3 @@
export enum WalletPageSelectors {
MagicRecoveryMessage = 'magic-recovery-message',
StatusMessage = 'status-message',
}
7 changes: 6 additions & 1 deletion tests/page-object-models/onboarding.page.ts
Expand Up @@ -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);
Expand Down

0 comments on commit 108fdf8

Please sign in to comment.