Skip to content

fix: import srp error handling and style #31662

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Apr 10, 2025
2 changes: 1 addition & 1 deletion test/e2e/page-objects/pages/account-list-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ class AccountListPage {
tag: 'h4',
};

private readonly importSrpInput = '#import-multi-srp__srp-word-0';
private readonly importSrpInput = '#import-srp__multi-srp__srp-word-0';

private readonly importSrpConfirmButton = {
text: 'Import wallet',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ jest.mock('../../../../store/actions', () => ({
const pasteSrpIntoFirstInput = (render: RenderResult, srp: string) => {
const [firstWord] = srp.split(' ');

const firstSrpWordDiv = render.getByTestId('import-multi-srp__srp-word-0');
const firstSrpWordDiv = render.getByTestId(
'import-srp__multi-srp__srp-word-0',
);
// This is safe because the input is always present in the word div.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const firstSrpWordInput = firstSrpWordDiv.querySelector('input')!;
Expand Down Expand Up @@ -132,6 +134,24 @@ describe('ImportSrp', () => {
expect(importButton).not.toBeEnabled();
});

it('shows 12 word seed phrase option', async () => {
const render = renderWithProvider(
<ImportSrp onActionComplete={jest.fn()} />,
store,
);
const { getByText, getByTestId } = render;

const twentyFourSeedWordOption = getByTestId(
'import-srp__multi-srp__switch-word-count-button',
);

fireEvent.click(twentyFourSeedWordOption);

await waitFor(async () => {
expect(getByText('I have a 12 word recovery phrase'));
});
});

it('calls addNewMnemonicToVault and showAlert on successful import', async () => {
const onActionComplete = jest.fn();
const render = renderWithProvider(
Expand Down Expand Up @@ -230,7 +250,7 @@ describe('ImportSrp', () => {
// Verify all input fields are cleared
for (let i = 0; i < 12; i++) {
const input = getByTestId(
`import-multi-srp__srp-word-${i}`,
`import-srp__multi-srp__srp-word-${i}`,
).querySelector('input');
expect(input).toHaveValue('');
}
Expand Down Expand Up @@ -278,7 +298,7 @@ describe('ImportSrp', () => {

// Verify that validation errors are present
const firstInput = getByTestId(
'import-multi-srp__srp-word-0',
'import-srp__multi-srp__srp-word-0',
).querySelector('input');
expect(firstInput).toBeInvalid();

Expand All @@ -289,4 +309,39 @@ describe('ImportSrp', () => {
// Verify that validation errors are cleared
expect(firstInput).not.toBeInvalid();
});

it('does not enable submit if 24 word seed was selected and 12 word seed was entered', async () => {
const render = renderWithProvider(
<ImportSrp onActionComplete={jest.fn()} />,
store,
);
const { getByText, getByTestId } = render;

const twentyFourSeedWordOption = getByTestId(
'import-srp__multi-srp__switch-word-count-button',
);

fireEvent.click(twentyFourSeedWordOption);

await waitFor(() => {
expect(
getByTestId('import-srp__multi-srp__srp-word-23').querySelector(
'input',
),
).toBeInTheDocument();
});

for (const [index, word] of VALID_SECRET_RECOVERY_PHRASE.split(
' ',
).entries()) {
// This is safe because the input is always present in the word div.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const input = getByTestId(
`import-srp__multi-srp__srp-word-${index}`,
).querySelector('input')!;
fireEvent.change(input, { target: { value: word } });
}

expect(getByText('Import wallet')).not.toBeEnabled();
});
});
172 changes: 100 additions & 72 deletions ui/components/multichain/multi-srp/import-srp/import-srp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@ import {
Display,
FlexDirection,
BorderRadius,
BackgroundColor,
} from '../../../../helpers/constants/design-system';
import { setShowNewSrpAddedToast } from '../../../app/toast-master/utils';
import { parseSecretRecoveryPhrase } from '../../../app/srp-input/parse-secret-recovery-phrase';
import { clearClipboard } from '../../../../helpers/utils/util';
import { useTheme } from '../../../../hooks/useTheme';
import { ThemeType } from '../../../../../shared/constants/preferences';

const hasUpperCase = (draftSrp: string) => {
return draftSrp !== draftSrp.toLowerCase();
Expand All @@ -38,6 +41,7 @@ export const ImportSrp = ({
onActionComplete: (completed: boolean) => void;
}) => {
const t = useI18nContext();
const theme = useTheme();
const dispatch = useDispatch();
const [srpError, setSrpError] = useState('');
const [pasteFailed, setPasteFailed] = useState(false);
Expand Down Expand Up @@ -72,6 +76,13 @@ export const ImportSrp = ({
return isValidMnemonic(secretRecoveryPhrase.join(' '));
}, [secretRecoveryPhrase]);

const hasEmptyWordsOrIncorrectLength = useMemo(() => {
return (
secretRecoveryPhrase.some((word) => word === '') ||
secretRecoveryPhrase.length !== numberOfWords
);
}, [secretRecoveryPhrase, numberOfWords]);

const onSrpChange = useCallback(
(newDraftSrp: string[]) => {
const validateSrp = (phrase: string[], words: boolean[]) => {
Expand Down Expand Up @@ -229,86 +240,103 @@ export const ImportSrp = ({
{t('importSRPDescription')}
</Text>

<Box
className="import-multi-srp__srp"
width={BlockSize.Full}
marginTop={4}
>
{Array.from({ length: numberOfWords }).map((_, index) => {
const id = `import-multi-srp__srp-word-${index}`;
return (
<Box
key={index}
display={Display.Flex}
flexDirection={FlexDirection.Row}
>
<Label
className="import-srp__multi-srp-label"
variant={TextVariant.bodyMdMedium}
marginRight={4}
<Box className="import-srp__multi-srp__srp-inner-container">
<Box
className="import-srp__multi-srp__srp"
width={BlockSize.Full}
marginTop={4}
>
{Array.from({ length: numberOfWords }).map((_, index) => {
const id = `import-srp__multi-srp__srp-word-${index}`;
return (
<Box
key={index}
display={Display.Flex}
flexDirection={FlexDirection.Row}
>
{index + 1}.
</Label>
<Box className="import-multi-srp__srp-word" marginBottom={4}>
<TextField
id={id}
data-testid={id}
borderRadius={BorderRadius.LG}
error={invalidSrpWords[index]}
type={TextFieldType.Text}
onChange={(e) => {
e.preventDefault();
onSrpWordChange(index, e.target.value);
}}
value={secretRecoveryPhrase[index]}
autoComplete={false}
onPaste={(event: React.ClipboardEvent) => {
const newSrp = event.clipboardData.getData('text');
<Label
className="import-srp__multi-srp__label"
variant={TextVariant.bodyMdMedium}
marginRight={4}
>
{index + 1}.
</Label>
<Box
className="import-srp__multi-srp__srp-word"
marginBottom={4}
>
<TextField
id={id}
data-testid={id}
borderRadius={BorderRadius.LG}
error={invalidSrpWords[index]}
type={TextFieldType.Text}
onChange={(e) => {
e.preventDefault();
onSrpWordChange(index, e.target.value);
}}
value={secretRecoveryPhrase[index]}
autoComplete={false}
onPaste={(event: React.ClipboardEvent) => {
const newSrp = event.clipboardData.getData('text');

if (newSrp.trim().match(/\s/u)) {
event.preventDefault();
onSrpPaste(newSrp);
}
}}
/>
if (newSrp.trim().match(/\s/u)) {
event.preventDefault();
onSrpPaste(newSrp);
}
}}
/>
</Box>
</Box>
</Box>
);
})}
</Box>
{srpError ? (
<BannerAlert
severity={BannerAlertSeverity.Danger}
description={srpError}
actionButtonLabel={t('clear')}
actionButtonOnClick={() => {
onSrpChange(Array(defaultNumberOfWords).fill(''));
setSrpError('');
}}
data-testid="bannerAlert"
/>
) : null}

{numberOfWords !== 24 && (
<Box width={BlockSize.Full} marginTop={4}>
<ButtonLink
width={BlockSize.Full}
loading={loading}
onClick={async () => {
setNumberOfWords(24);
);
})}
</Box>
{srpError ? (
<BannerAlert
severity={BannerAlertSeverity.Danger}
description={srpError}
actionButtonLabel={t('clear')}
actionButtonOnClick={() => {
onSrpChange(Array(defaultNumberOfWords).fill(''));
setSrpError('');
setInvalidSrpWords(Array(24).fill(false));
}}
>
{t('importNWordSRP', ['24'])}
</ButtonLink>
</Box>
)}
data-testid="bannerAlert"
/>
) : null}

<Box width={BlockSize.Full} marginTop={4}>
{
<Box width={BlockSize.Full} marginTop={4}>
<ButtonLink
width={BlockSize.Full}
loading={loading}
onClick={async () => {
setNumberOfWords(numberOfWords === 12 ? 24 : 12);
setSrpError('');
setInvalidSrpWords(
Array(numberOfWords === 12 ? 24 : 12).fill(false),
);
}}
data-testid="import-srp__multi-srp__switch-word-count-button"
>
{t('importNWordSRP', [numberOfWords === 12 ? '24' : '12'])}
</ButtonLink>
</Box>
}
</Box>
<Box
className="import-srp__multi-srp__import-button"
width={BlockSize.Full}
marginTop={4}
paddingBottom={6}
backgroundColor={
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Can we add padding at the top of the button as well?

Current Example
current example

theme === ThemeType.light
? BackgroundColor.backgroundDefault
: BackgroundColor.backgroundDefault
}
>
<ButtonPrimary
width={BlockSize.Full}
disabled={!isValidSrp}
disabled={!isValidSrp || hasEmptyWordsOrIncorrectLength}
loading={loading}
onClick={async () => {
try {
Expand Down
17 changes: 15 additions & 2 deletions ui/components/multichain/multi-srp/import-srp/index.scss
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
@use "design-system";

.import-multi-srp {
.import-srp__multi-srp {
&__srp-inner-container {
padding-bottom: 64px;
overflow-y: auto;
}

&__srp {
display: grid;
grid-template-columns: 1fr 1fr;
grid-area: input;
gap: 0 24px;
}

&__multi-srp-label {
&__label {
min-width: 16px;
max-width: 16px;
flex-shrink: 0;
Expand All @@ -23,4 +28,12 @@
flex: 1;
}
}

// This makes the import button float at the bottom of the screen.
&__import-button {
position: fixed;
bottom: 0px;
z-index: 99999;
max-width: calc(100% - 32px);
}
}
Loading