9 changes: 0 additions & 9 deletions app/_locales/it/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 0 additions & 9 deletions app/_locales/ja/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions app/_locales/kn/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 0 additions & 9 deletions app/_locales/ko/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions app/_locales/lt/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions app/_locales/lv/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions app/_locales/ms/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions app/_locales/no/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 0 additions & 9 deletions app/_locales/ph/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions app/_locales/pl/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 0 additions & 9 deletions app/_locales/pt_BR/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions app/_locales/ro/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 0 additions & 9 deletions app/_locales/ru/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions app/_locales/sk/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions app/_locales/sl/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions app/_locales/sr/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions app/_locales/sv/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions app/_locales/sw/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 0 additions & 9 deletions app/_locales/tl/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 0 additions & 9 deletions app/_locales/tr/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions app/_locales/uk/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 0 additions & 9 deletions app/_locales/vi/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 0 additions & 9 deletions app/_locales/zh_CN/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions app/_locales/zh_TW/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "metamask-crx",
"version": "10.11.2",
"version": "10.11.3",
"private": true,
"repository": {
"type": "git",
Expand Down Expand Up @@ -83,8 +83,8 @@
"3box/ipfs/ipld-zcash/zcash-bitcore-lib/elliptic": "^6.5.4",
"3box/ipfs/libp2p-mdns/multicast-dns/dns-packet": "^5.2.2",
"3box/ipfs/prometheus-gc-stats/gc-stats/node-pre-gyp/tar": "^6.1.2",
"3box/**/libp2p-crypto/node-forge": "^1.0.0",
"3box/**/libp2p-keychain/node-forge": "^1.0.0",
"3box/**/libp2p-crypto/node-forge": "^1.3.0",
"3box/**/libp2p-keychain/node-forge": "^1.3.0",
"3box/ipfs/libp2p-webrtc-star/socket.io/engine.io": "^4.0.0",
"analytics-node/axios": "^0.21.2",
"ganache-core/lodash": "^4.17.21",
Expand Down Expand Up @@ -261,6 +261,7 @@
"@testing-library/jest-dom": "^5.11.10",
"@testing-library/react": "^10.4.8",
"@testing-library/react-hooks": "^3.2.1",
"@testing-library/user-event": "^14.0.0-beta.12",
"@types/react": "^16.9.53",
"addons-linter": "1.14.0",
"babelify": "^10.0.0",
Expand All @@ -269,7 +270,7 @@
"browser-util-inspect": "^0.2.0",
"browserify": "^16.5.1",
"chalk": "^3.0.0",
"chromedriver": "^98.0.0",
"chromedriver": "^99.0.0",
"concurrently": "^5.2.0",
"copy-webpack-plugin": "^6.0.3",
"cross-spawn": "^7.0.3",
Expand Down
5 changes: 2 additions & 3 deletions test/e2e/metamask-ui.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,11 +200,10 @@ describe('MetaMask', function () {
await restoreSeedLink.click();
await driver.delay(regularDelayMs);

await driver.fill(
'input[placeholder="Enter your Secret Recovery Phrase"]',
await driver.pasteIntoField(
'[data-testid="import-srp__srp-word-0"]',
testSeedPhrase,
);
await driver.delay(regularDelayMs);

await driver.fill('#password', 'correct horse battery staple');
await driver.fill('#confirm-password', 'correct horse battery staple');
Expand Down
4 changes: 2 additions & 2 deletions test/e2e/tests/from-import-ui.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ describe('Metamask Import UI', function () {
await driver.clickElement('.btn-secondary');

// Import Secret Recovery Phrase
await driver.fill(
'input[placeholder="Enter your Secret Recovery Phrase"]',
await driver.pasteIntoField(
'[data-testid="import-srp__srp-word-0"]',
testSeedPhrase,
);

Expand Down
4 changes: 2 additions & 2 deletions test/e2e/tests/metamask-responsive-ui.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,8 @@ describe('Metamask Responsive UI', function () {
);
await restoreSeedLink.click();

await driver.fill(
'input[placeholder="Enter your Secret Recovery Phrase"]',
await driver.pasteIntoField(
'[data-testid="import-srp__srp-word-0"]',
testSeedPhrase,
);

Expand Down
22 changes: 21 additions & 1 deletion test/e2e/webdriver/driver.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const { promises: fs } = require('fs');
const { strict: assert } = require('assert');
const { until, error: webdriverError, By } = require('selenium-webdriver');
const { until, error: webdriverError, By, Key } = require('selenium-webdriver');
const cssToXPath = require('css-to-xpath');

/**
Expand Down Expand Up @@ -248,6 +248,26 @@ class Driver {
assert.ok(!dataTab, 'Found element that should not be present');
}

/**
* Paste a string into a field.
*
* @param {string} element - The element locator.
* @param {string} contentToPaste - The content to paste.
*/
async pasteIntoField(element, contentToPaste) {
// Throw if double-quote is present in content to paste
// so that we don't have to worry about escaping double-quotes
if (contentToPaste.includes('"')) {
throw new Error('Cannot paste content with double-quote');
}
// Click to focus the field
await this.clickElement(element);
await this.executeScript(
`navigator.clipboard.writeText("${contentToPaste}")`,
);
await this.fill(element, Key.chord(Key.CONTROL, 'v'));
}

// Navigation

async navigate(page = Driver.PAGES.HOME) {
Expand Down
8 changes: 8 additions & 0 deletions test/helpers/setup-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,11 @@ if (!window.crypto.getRandomValues) {
// eslint-disable-next-line node/global-require
window.crypto.getRandomValues = require('polyfill-crypto.getrandomvalues');
}

// Used to test `clearClipboard` function
if (!window.navigator.clipboard) {
window.navigator.clipboard = {};
}
if (!window.navigator.clipboard.writeText) {
window.navigator.clipboard.writeText = () => undefined;
}
14 changes: 14 additions & 0 deletions test/lib/render-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,17 @@ export function renderWithProvider(component, store) {

return render(component, { wrapper: Wrapper });
}

export function renderWithLocalization(component) {
const Wrapper = ({ children }) => (
<I18nProvider currentLocale="en" current={en} en={en}>
<LegacyI18nProvider>{children}</LegacyI18nProvider>
</I18nProvider>
);

Wrapper.propTypes = {
children: PropTypes.node,
};

return render(component, { wrapper: Wrapper });
}
1 change: 1 addition & 0 deletions ui/components/app/app-components.scss
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
@import 'selected-account/index';
@import 'signature-request/index';
@import 'signature-request-original/index';
@import 'srp-input/srp-input';
@import 'tab-bar/index';
@import 'token-cell/token-cell';
@import 'token-list-display/token-list-display';
Expand Down
135 changes: 27 additions & 108 deletions ui/components/app/create-new-vault/create-new-vault.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
import { ethers } from 'ethers';
import React, { useCallback, useContext, useState } from 'react';
import PropTypes from 'prop-types';
import { useI18nContext } from '../../../hooks/useI18nContext';
import { MetaMetricsContext } from '../../../contexts/metametrics';
import TextField from '../../ui/text-field';
import Button from '../../ui/button';
import { clearClipboard } from '../../../helpers/utils/util';
import CheckBox from '../../ui/check-box';
import Typography from '../../ui/typography';
import { COLORS } from '../../../helpers/constants/design-system';
import { parseSecretRecoveryPhrase } from './parse-secret-recovery-phrase';

const { isValidMnemonic } = ethers.utils;
import SrpInput from '../srp-input';

export default function CreateNewVault({
disabled = false,
Expand All @@ -24,33 +19,11 @@ export default function CreateNewVault({
const [password, setPassword] = useState('');
const [passwordError, setPasswordError] = useState('');
const [seedPhrase, setSeedPhrase] = useState('');
const [seedPhraseError, setSeedPhraseError] = useState('');
const [showSeedPhrase, setShowSeedPhrase] = useState(false);
const [termsChecked, setTermsChecked] = useState(false);

const t = useI18nContext();
const metricsEvent = useContext(MetaMetricsContext);

const onSeedPhraseChange = useCallback(
(rawSeedPhrase) => {
let newSeedPhraseError = '';

if (rawSeedPhrase) {
const parsedSeedPhrase = parseSecretRecoveryPhrase(rawSeedPhrase);
const wordCount = parsedSeedPhrase.split(/\s/u).length;
if (wordCount % 3 !== 0 || wordCount > 24 || wordCount < 12) {
newSeedPhraseError = t('seedPhraseReq');
} else if (!isValidMnemonic(parsedSeedPhrase)) {
newSeedPhraseError = t('invalidSeedPhrase');
}
}

setSeedPhrase(rawSeedPhrase);
setSeedPhraseError(newSeedPhraseError);
},
[setSeedPhrase, setSeedPhraseError, t],
);

const onPasswordChange = useCallback(
(newPassword) => {
let newConfirmPasswordError = '';
Expand Down Expand Up @@ -93,8 +66,7 @@ export default function CreateNewVault({
seedPhrase &&
(!includeTerms || termsChecked) &&
!passwordError &&
!confirmPasswordError &&
!seedPhraseError;
!confirmPasswordError;

const onImport = useCallback(
async (event) => {
Expand All @@ -104,7 +76,7 @@ export default function CreateNewVault({
return;
}

await onSubmit(password, parseSecretRecoveryPhrase(seedPhrase));
await onSubmit(password, seedPhrase);
},
[isValid, onSubmit, password, seedPhrase],
);
Expand All @@ -121,10 +93,6 @@ export default function CreateNewVault({
setTermsChecked((currentTermsChecked) => !currentTermsChecked);
}, [metricsEvent]);

const toggleShowSeedPhrase = useCallback(() => {
setShowSeedPhrase((currentShowSeedPhrase) => !currentShowSeedPhrase);
}, []);

const termsOfUse = t('acceptTermsOfUse', [
<a
className="create-new-vault__terms-link"
Expand All @@ -139,80 +107,31 @@ export default function CreateNewVault({

return (
<form className="create-new-vault__form" onSubmit={onImport}>
<div className="create-new-vault__srp-section">
<label
htmlFor="create-new-vault__srp"
className="create-new-vault__srp-label"
>
<Typography>{t('secretRecoveryPhrase')}</Typography>
</label>
{showSeedPhrase ? (
<textarea
id="create-new-vault__srp"
className="create-new-vault__srp-shown"
onChange={(e) => onSeedPhraseChange(e.target.value)}
onPaste={clearClipboard}
value={seedPhrase}
placeholder={t('seedPhrasePlaceholder')}
autoComplete="off"
/>
) : (
<TextField
id="create-new-vault__srp"
type="password"
onChange={(e) => onSeedPhraseChange(e.target.value)}
value={seedPhrase}
placeholder={t('seedPhrasePlaceholderPaste')}
autoComplete="off"
onPaste={clearClipboard}
/>
)}
{seedPhraseError ? (
<Typography
color={COLORS.ERROR1}
tag="span"
className="create-new-vault__srp-error"
>
{seedPhraseError}
</Typography>
) : null}
<div className="create-new-vault__show-srp">
<CheckBox
id="create-new-vault__show-srp-checkbox"
checked={showSeedPhrase}
onClick={toggleShowSeedPhrase}
title={t('showSeedPhrase')}
/>
<label
className="create-new-vault__show-srp-label"
htmlFor="create-new-vault__show-srp-checkbox"
>
<Typography tag="span">{t('showSeedPhrase')}</Typography>
</label>
</div>
<SrpInput onChange={setSeedPhrase} />
<div className="create-new-vault__create-password">
<TextField
id="password"
label={t('newPassword')}
type="password"
value={password}
onChange={(event) => onPasswordChange(event.target.value)}
error={passwordError}
autoComplete="new-password"
margin="normal"
largeLabel
/>
<TextField
id="confirm-password"
label={t('confirmPassword')}
type="password"
value={confirmPassword}
onChange={(event) => onConfirmPasswordChange(event.target.value)}
error={confirmPasswordError}
autoComplete="new-password"
margin="normal"
largeLabel
/>
</div>
<TextField
id="password"
label={t('newPassword')}
type="password"
value={password}
onChange={(event) => onPasswordChange(event.target.value)}
error={passwordError}
autoComplete="new-password"
margin="normal"
largeLabel
/>
<TextField
id="confirm-password"
label={t('confirmPassword')}
type="password"
value={confirmPassword}
onChange={(event) => onConfirmPasswordChange(event.target.value)}
error={confirmPasswordError}
autoComplete="new-password"
margin="normal"
largeLabel
/>
{includeTerms ? (
<div className="create-new-vault__terms">
<CheckBox
Expand Down
27 changes: 2 additions & 25 deletions ui/components/app/create-new-vault/create-new-vault.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,12 @@
&__form {
display: flex;
flex-direction: column;
width: 360px;
}

&__srp-shown {
@include Paragraph;

padding: 8px 16px;
}

&__srp-section {
&__create-password {
display: flex;
flex-direction: column;
}

&__srp-label {
margin-bottom: 8px;
}

&__srp-error {
margin-top: 4px;
}

&__show-srp {
margin-top: 16px;
margin-bottom: 16px;
}

&__show-srp-label {
margin-left: 8px;
width: 360px;
}

&__terms {
Expand Down
1 change: 1 addition & 0 deletions ui/components/app/srp-input/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './srp-input';
File renamed without changes.
File renamed without changes.
235 changes: 235 additions & 0 deletions ui/components/app/srp-input/srp-input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
import { ethers } from 'ethers';
import React, { useCallback, useState } from 'react';
import PropTypes from 'prop-types';
import { useI18nContext } from '../../../hooks/useI18nContext';
import TextField from '../../ui/text-field';
import { clearClipboard } from '../../../helpers/utils/util';
import ActionableMessage from '../../ui/actionable-message';
import Dropdown from '../../ui/dropdown';
import Typography from '../../ui/typography';
import ShowHideToggle from '../../ui/show-hide-toggle';
import { TYPOGRAPHY } from '../../../helpers/constants/design-system';
import { parseSecretRecoveryPhrase } from './parse-secret-recovery-phrase';

const { isValidMnemonic } = ethers.utils;

const defaultNumberOfWords = 12;

export default function SrpInput({ onChange }) {
const [srpError, setSrpError] = useState('');
const [pasteFailed, setPasteFailed] = useState(false);
const [draftSrp, setDraftSrp] = useState(
new Array(defaultNumberOfWords).fill(''),
);
const [showSrp, setShowSrp] = useState(
new Array(defaultNumberOfWords).fill(false),
);
const [numberOfWords, setNumberOfWords] = useState(defaultNumberOfWords);

const t = useI18nContext();

const onSrpChange = useCallback(
(newDraftSrp) => {
let newSrpError = '';
const joinedDraftSrp = newDraftSrp.join(' ');

if (newDraftSrp.some((word) => word !== '')) {
if (newDraftSrp.some((word) => word === '')) {
newSrpError = t('seedPhraseReq');
} else if (!isValidMnemonic(joinedDraftSrp)) {
newSrpError = t('invalidSeedPhrase');
}
}

setDraftSrp(newDraftSrp);
setSrpError(newSrpError);
onChange(newSrpError ? '' : joinedDraftSrp);
},
[setDraftSrp, setSrpError, t, onChange],
);

const toggleShowSrp = useCallback((index) => {
setShowSrp((currentShowSrp) => {
const newShowSrp = currentShowSrp.slice();
if (newShowSrp[index]) {
newShowSrp[index] = false;
} else {
newShowSrp.fill(false);
newShowSrp[index] = true;
}
return newShowSrp;
});
}, []);

const onSrpWordChange = useCallback(
(index, newWord) => {
if (pasteFailed) {
setPasteFailed(false);
}
const newSrp = draftSrp.slice();
newSrp[index] = newWord.trim();
onSrpChange(newSrp);
},
[draftSrp, onSrpChange, pasteFailed],
);

const onSrpPaste = useCallback(
(rawSrp) => {
const parsedSrp = parseSecretRecoveryPhrase(rawSrp);
let newDraftSrp = parsedSrp.split(' ');

if (newDraftSrp.length > 24) {
setPasteFailed(true);
return;
} else if (pasteFailed) {
setPasteFailed(false);
}

let newNumberOfWords = numberOfWords;
if (newDraftSrp.length !== numberOfWords) {
if (newDraftSrp.length < 12) {
newNumberOfWords = 12;
} else if (newDraftSrp.length % 3 === 0) {
newNumberOfWords = newDraftSrp.length;
} else {
newNumberOfWords =
newDraftSrp.length + (3 - (newDraftSrp.length % 3));
}
setNumberOfWords(newNumberOfWords);
}

if (newDraftSrp.length < newNumberOfWords) {
newDraftSrp = newDraftSrp.concat(
new Array(newNumberOfWords - newDraftSrp.length).fill(''),
);
}
setShowSrp(new Array(newNumberOfWords).fill(false));
onSrpChange(newDraftSrp);
clearClipboard();
},
[numberOfWords, onSrpChange, pasteFailed, setPasteFailed],
);

const numberOfWordsOptions = [];
for (let i = 12; i <= 24; i += 3) {
numberOfWordsOptions.push({
name: t('srpInputNumberOfWords', [`${i}`]),
value: `${i}`,
});
}

return (
<div className="import-srp__container">
<label className="import-srp__srp-label">
<Typography variant={TYPOGRAPHY.H4}>
{t('secretRecoveryPhrase')}
</Typography>
</label>
<ActionableMessage
className="import-srp__paste-tip"
iconFillColor="#037dd6" // This is `--color-info-default`
message={t('srpPasteTip')}
useIcon
/>
<Dropdown
className="import-srp__number-of-words-dropdown"
onChange={(newSelectedOption) => {
const newNumberOfWords = parseInt(newSelectedOption, 10);
if (Number.isNaN(newNumberOfWords)) {
throw new Error('Unable to parse option as integer');
}

let newDraftSrp = draftSrp.slice(0, newNumberOfWords);
if (newDraftSrp.length < newNumberOfWords) {
newDraftSrp = newDraftSrp.concat(
new Array(newNumberOfWords - newDraftSrp.length).fill(''),
);
}
setNumberOfWords(newNumberOfWords);
setShowSrp(new Array(newNumberOfWords).fill(false));
onSrpChange(newDraftSrp);
}}
options={numberOfWordsOptions}
selectedOption={`${numberOfWords}`}
/>
<div className="import-srp__srp">
{[...Array(numberOfWords).keys()].map((index) => {
const id = `import-srp__srp-word-${index}`;
return (
<div key={index} className="import-srp__srp-word">
<label htmlFor={id} className="import-srp__srp-word-label">
<Typography>{`${index + 1}.`}</Typography>
</label>
<TextField
id={id}
data-testid={id}
type={showSrp[index] ? 'text' : 'password'}
onChange={(e) => {
e.preventDefault();
onSrpWordChange(index, e.target.value);
}}
value={draftSrp[index]}
autoComplete="off"
onPaste={(event) => {
const newSrp = event.clipboardData.getData('text');

if (newSrp.trim().match(/\s/u)) {
event.preventDefault();
onSrpPaste(newSrp);
} else {
onSrpWordChange(index, newSrp);
}
}}
/>
<ShowHideToggle
id={`${id}-checkbox`}
ariaLabelHidden={t('srpWordHidden')}
ariaLabelShown={t('srpWordShown')}
shown={showSrp[index]}
data-testid={`${id}-checkbox`}
onChange={() => toggleShowSrp(index)}
title={t('srpToggleShow')}
/>
</div>
);
})}
</div>
{srpError ? (
<ActionableMessage
className="import-srp__srp-error"
iconFillColor="#d73a49" // This is `--color-error-default`
message={srpError}
type="danger"
useIcon
/>
) : null}
{pasteFailed ? (
<ActionableMessage
className="import-srp__srp-too-many-words-error"
iconFillColor="#d73a49" // This is `--color-error-default`
message={t('srpPasteFailedTooManyWords')}
primaryAction={{
label: t('dismiss'),
onClick: () => setPasteFailed(false),
}}
type="danger"
useIcon
/>
) : null}
</div>
);
}

SrpInput.propTypes = {
/**
* Event handler for SRP changes.
*
* This is only called with a valid, well-formated (i.e. exactly one space
* between each word) SRP or with an empty string.
*
* This is called each time the draft SRP is updated. If the draft SRP is
* valid, this is called with a well-formatted version of that draft SRP.
* Otherwise, this is called with an empty string.
*/
onChange: PropTypes.func.isRequired,
};
71 changes: 71 additions & 0 deletions ui/components/app/srp-input/srp-input.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
.import-srp {
&__container {
display: grid;
grid-template-areas:
"title dropdown"
"paste-tip paste-tip"
"input input"
"error error"
"too-many-words-error too-many-words-error";
}

@media (max-width: 767px) {
&__container {
grid-template-areas:
"title"
"dropdown"
"paste-tip"
"input"
"error"
"too-many-words-error";
}
}

&__srp-label {
grid-area: title;
}

&__number-of-words-dropdown {
grid-area: dropdown;
}

&__paste-tip {
margin-bottom: 8px;
grid-area: paste-tip;
width: auto;
margin-left: auto;
margin-right: auto;
}

&__srp {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-area: input;
}

@media (max-width: 767px) {
&__srp {
grid-template-columns: 1fr;
}
}

&__srp-word {
display: flex;
align-items: center;
margin: 8px;
}

&__srp-word-label {
width: 2em;
}

&__srp-error {
margin-top: 4px;
grid-area: error;
}

&__srp-too-many-words-error {
margin-top: 4px;
grid-area: too-many-words-error;
}
}
19 changes: 19 additions & 0 deletions ui/components/app/srp-input/srp-input.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react';
import SrpInput from '.';

export default {
title: 'Components/App/SrpInput',
id: __filename,
component: SrpInput,
argTypes: {
onChange: { action: 'changed' },
},
};

const Template = (args) => {
return <SrpInput {...args} />;
};

export const DefaultStory = Template.bind({});

DefaultStory.storyName = 'Default';
1,676 changes: 1,676 additions & 0 deletions ui/components/app/srp-input/srp-input.test.js

Large diffs are not rendered by default.

43 changes: 43 additions & 0 deletions ui/components/ui/icon/icon-eye-slash.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React from 'react';
import PropTypes from 'prop-types';

const IconEyeSlash = ({
size = 24,
color = 'currentColor',
ariaLabel,
className,
}) => (
// This SVG is copied from `@fortawesome/fontawesome-free@5.13.0/regular/eye-slash.svg`.
<svg
width={size}
height={size}
fill={color}
className={className}
aria-label={ariaLabel}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 640 512"
>
<path d="M634 471L36 3.51A16 16 0 0 0 13.51 6l-10 12.49A16 16 0 0 0 6 41l598 467.49a16 16 0 0 0 22.49-2.49l10-12.49A16 16 0 0 0 634 471zM296.79 146.47l134.79 105.38C429.36 191.91 380.48 144 320 144a112.26 112.26 0 0 0-23.21 2.47zm46.42 219.07L208.42 260.16C210.65 320.09 259.53 368 320 368a113 113 0 0 0 23.21-2.46zM320 112c98.65 0 189.09 55 237.93 144a285.53 285.53 0 0 1-44 60.2l37.74 29.5a333.7 333.7 0 0 0 52.9-75.11 32.35 32.35 0 0 0 0-29.19C550.29 135.59 442.93 64 320 64c-36.7 0-71.71 7-104.63 18.81l46.41 36.29c18.94-4.3 38.34-7.1 58.22-7.1zm0 288c-98.65 0-189.08-55-237.93-144a285.47 285.47 0 0 1 44.05-60.19l-37.74-29.5a333.6 333.6 0 0 0-52.89 75.1 32.35 32.35 0 0 0 0 29.19C89.72 376.41 197.08 448 320 448c36.7 0 71.71-7.05 104.63-18.81l-46.41-36.28C359.28 397.2 339.89 400 320 400z" />
</svg>
);

IconEyeSlash.propTypes = {
/**
* The size of the Icon follows an 8px grid 2 = 16px, 3 = 24px etc
*/
size: PropTypes.number,
/**
* The color of the icon accepts design token css variables
*/
color: PropTypes.string,
/**
* An additional className to assign the Icon
*/
className: PropTypes.string,
/**
* The aria-label of the icon for accessibility purposes
*/
ariaLabel: PropTypes.string,
};

export default IconEyeSlash;
43 changes: 43 additions & 0 deletions ui/components/ui/icon/icon-eye.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React from 'react';
import PropTypes from 'prop-types';

const IconEye = ({
size = 24,
color = 'currentColor',
ariaLabel,
className,
}) => (
// This SVG copied from `@fortawesome/fontawesome-free@5.13.0/regular/eye.svg`.
<svg
width={size}
height={size}
fill={color}
className={className}
aria-label={ariaLabel}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 576 512"
>
<path d="M288 144a110.94 110.94 0 0 0-31.24 5 55.4 55.4 0 0 1 7.24 27 56 56 0 0 1-56 56 55.4 55.4 0 0 1-27-7.24A111.71 111.71 0 1 0 288 144zm284.52 97.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400c-98.65 0-189.09-55-237.93-144C98.91 167 189.34 112 288 112s189.09 55 237.93 144C477.1 345 386.66 400 288 400z" />
</svg>
);

IconEye.propTypes = {
/**
* The size of the Icon follows an 8px grid 2 = 16px, 3 = 24px etc
*/
size: PropTypes.number,
/**
* The color of the icon accepts design token css variables
*/
color: PropTypes.string,
/**
* An additional className to assign the Icon
*/
className: PropTypes.string,
/**
* The aria-label of the icon for accessibility purposes
*/
ariaLabel: PropTypes.string,
};

export default IconEye;
4 changes: 4 additions & 0 deletions ui/components/ui/icon/icon.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import SunCheck from './sun-check-icon.component';
import Swap from './swap-icon-for-list.component';
import SwapIcon from './overview-send-icon.component';
import SwapIconComponent from './swap-icon.component';
import IconEye from './icon-eye';
import IconEyeSlash from './icon-eye-slash';

export default {
title: 'Components/UI/Icon',
Expand Down Expand Up @@ -104,6 +106,8 @@ export const DefaultStory = (args) => (
<IconItem Component={<PaperAirplane {...args} />} />
<IconItem Component={<CopyIcon {...args} />} />
<IconItem Component={<Preloader {...args} />} />
<IconItem Component={<IconEye {...args} />} />
<IconItem Component={<IconEyeSlash {...args} />} />
</div>
</Box>
</div>
Expand Down
1 change: 1 addition & 0 deletions ui/components/ui/show-hide-toggle/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './show-hide-toggle';
34 changes: 34 additions & 0 deletions ui/components/ui/show-hide-toggle/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
.show-hide-toggle {
position: relative;
display: inline-flex;

&__input {
appearance: none;

+ .show-hide-toggle__label {
cursor: pointer;
user-select: none;
}

/* Focused when tabbing with keyboard */
&:focus,
&:focus-visible {
outline: none;

+ .show-hide-toggle__label {
outline: Highlight auto 1px;
}
}

&:disabled {
+ label {
opacity: 0.5;
cursor: auto;
}
}
}

&__icon {
color: var(--color-icon-default);
}
}
86 changes: 86 additions & 0 deletions ui/components/ui/show-hide-toggle/show-hide-toggle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';

import IconEye from '../icon/icon-eye';
import IconEyeSlash from '../icon/icon-eye-slash';

const ShowHideToggle = ({
id,
shown,
onChange,
ariaLabelHidden,
ariaLabelShown,
className,
'data-testid': dataTestId,
disabled,
title,
}) => {
return (
<div className={classnames('show-hide-toggle', className)}>
<input
className="show-hide-toggle__input"
id={id}
type="checkbox"
checked={shown}
onChange={onChange}
data-testid={dataTestId}
disabled={disabled}
/>
<label htmlFor={id} className="show-hide-toggle__label" title={title}>
{shown ? (
<IconEye
ariaLabel={ariaLabelShown}
className="show-hide-toggle__icon"
/>
) : (
<IconEyeSlash
ariaLabel={ariaLabelHidden}
className="show-hide-toggle__icon"
/>
)}
</label>
</div>
);
};

ShowHideToggle.propTypes = {
/**
* The id of the ShowHideToggle for htmlFor
*/
id: PropTypes.string.isRequired,
/**
* If the ShowHideToggle is in the "shown" state or not
*/
shown: PropTypes.bool.isRequired,
/**
* The onChange handler of the ShowHideToggle
*/
onChange: PropTypes.func.isRequired,
/**
* The aria-label of the icon representing the "hidden" state
*/
ariaLabelHidden: PropTypes.string.isRequired,
/**
* The aria-label of the icon representing the "shown" state
*/
ariaLabelShown: PropTypes.string.isRequired,
/**
* An additional className to give the ShowHideToggle
*/
className: PropTypes.string,
/**
* The data test id of the input
*/
'data-testid': PropTypes.string,
/**
* Whether the input is disabled or not
*/
disabled: PropTypes.bool,
/**
* The title for the toggle. This is shown in a tooltip on hover.
*/
title: PropTypes.string,
};

export default ShowHideToggle;
51 changes: 51 additions & 0 deletions ui/components/ui/show-hide-toggle/show-hide-toggle.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from 'react';
import { useArgs } from '@storybook/client-api';
import ShowHideToggle from '.';

export default {
title: 'Components/UI/ShowHideToggle', // title should follow the folder structure location of the component. Don't use spaces.
id: __filename,
argTypes: {
id: {
control: 'text',
},
ariaLabelHidden: {
control: 'text',
},
ariaLabelShown: {
control: 'text',
},
className: {
control: 'text',
},
dataTestId: {
control: 'text',
},
disabled: {
control: 'boolean',
},
onChange: {
action: 'onChange',
},
shown: {
control: 'boolean',
},
},
};

export const DefaultStory = (args) => {
const [{ shown }, updateArgs] = useArgs();
const handleOnToggle = () => {
updateArgs({ shown: !shown });
};
return <ShowHideToggle {...args} shown={shown} onChange={handleOnToggle} />;
};

DefaultStory.args = {
id: 'showHideToggle',
ariaLabelHidden: 'hidden',
ariaLabelShown: 'shown',
shown: false,
};

DefaultStory.storyName = 'Default';
314 changes: 314 additions & 0 deletions ui/components/ui/show-hide-toggle/show-hide-toggle.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,314 @@
import React from 'react';
import { isInaccessible, render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import ShowHideToggle from '.';

describe('ShowHideToggle', () => {
beforeEach(() => {
jest.resetAllMocks();
});

it('should set title', async () => {
const onChange = jest.fn();
const { queryByTitle } = render(
<ShowHideToggle
id="example"
ariaLabelHidden="hidden"
ariaLabelShown="shown"
shown
onChange={onChange}
title="example-title"
/>,
);

expect(queryByTitle('example-title')).toBeInTheDocument();
});

it('should set test ID', async () => {
const onChange = jest.fn();
const { queryByTestId } = render(
<ShowHideToggle
id="example"
ariaLabelHidden="hidden"
ariaLabelShown="shown"
shown
onChange={onChange}
data-testid="example-test-id"
/>,
);

expect(queryByTestId('example-test-id')).toBeInTheDocument();
});

it('should show correct aria-label when shown', () => {
const onChange = jest.fn();
const { queryByLabelText } = render(
<ShowHideToggle
id="example"
ariaLabelHidden="hidden"
ariaLabelShown="shown"
shown
onChange={onChange}
/>,
);

expect(queryByLabelText('hidden')).not.toBeInTheDocument();
expect(queryByLabelText('shown')).toBeInTheDocument();
});

it('should show correct aria-label when hidden', () => {
const onChange = jest.fn();
const { queryByLabelText } = render(
<ShowHideToggle
id="example"
ariaLabelHidden="hidden"
ariaLabelShown="shown"
shown={false}
onChange={onChange}
/>,
);

expect(queryByLabelText('hidden')).toBeInTheDocument();
expect(queryByLabelText('shown')).not.toBeInTheDocument();
});

it('should show correct checkbox state when shown', () => {
const onChange = jest.fn();
const { queryByRole } = render(
<ShowHideToggle
id="example"
ariaLabelHidden="hidden"
ariaLabelShown="shown"
shown
onChange={onChange}
/>,
);

expect(queryByRole('checkbox')).toBeChecked();
});

it('should show correct checkbox state when hidden', () => {
const onChange = jest.fn();
const { queryByRole } = render(
<ShowHideToggle
id="example"
ariaLabelHidden="hidden"
ariaLabelShown="shown"
shown={false}
onChange={onChange}
/>,
);

expect(queryByRole('checkbox')).not.toBeChecked();
});

describe('enabled', () => {
it('should show checkbox as enabled', () => {
const onChange = jest.fn();
const { queryByRole } = render(
<ShowHideToggle
id="example"
ariaLabelHidden="hidden"
ariaLabelShown="shown"
shown
onChange={onChange}
/>,
);

expect(queryByRole('checkbox')).toBeEnabled();
});

it('should be accessible', () => {
const onChange = jest.fn();
const { queryByRole } = render(
<ShowHideToggle
id="example"
ariaLabelHidden="hidden"
ariaLabelShown="shown"
shown
onChange={onChange}
/>,
);

expect(isInaccessible(queryByRole('checkbox'))).toBeFalsy();
});

describe('shown', () => {
it('should call onChange when clicked', async () => {
const onChange = jest.fn();
const { queryByRole } = render(
<ShowHideToggle
id="example"
ariaLabelHidden="hidden"
ariaLabelShown="shown"
shown
onChange={onChange}
/>,
);
await userEvent.click(queryByRole('checkbox'));

expect(onChange).toHaveBeenCalledTimes(1);
});

it('should call onChange on space', async () => {
const onChange = jest.fn();
const { queryByRole } = render(
<ShowHideToggle
id="example"
ariaLabelHidden="hidden"
ariaLabelShown="shown"
shown
onChange={onChange}
/>,
);
queryByRole('checkbox').focus();
await userEvent.keyboard('[Space]');

expect(onChange).toHaveBeenCalledTimes(1);
});
});

describe('hidden', () => {
it('should call onChange when clicked', async () => {
const onChange = jest.fn();
const { queryByRole } = render(
<ShowHideToggle
id="example"
ariaLabelHidden="hidden"
ariaLabelShown="shown"
shown={false}
onChange={onChange}
/>,
);
await userEvent.click(queryByRole('checkbox'));

expect(onChange).toHaveBeenCalledTimes(1);
});

it('should call onChange on space', async () => {
const onChange = jest.fn();
const { queryByRole } = render(
<ShowHideToggle
id="example"
ariaLabelHidden="hidden"
ariaLabelShown="shown"
shown={false}
onChange={onChange}
/>,
);
queryByRole('checkbox').focus();
await userEvent.keyboard('[Space]');

expect(onChange).toHaveBeenCalledTimes(1);
});
});
});

describe('disabled', () => {
it('should show checkbox as disabled', () => {
const onChange = jest.fn();
const { queryByRole } = render(
<ShowHideToggle
id="example"
ariaLabelHidden="hidden"
ariaLabelShown="shown"
shown
disabled
onChange={onChange}
/>,
);

expect(queryByRole('checkbox')).toBeDisabled();
});

it('should be accessible', () => {
const onChange = jest.fn();
const { queryByRole } = render(
<ShowHideToggle
id="example"
ariaLabelHidden="hidden"
ariaLabelShown="shown"
shown
disabled
onChange={onChange}
/>,
);

expect(isInaccessible(queryByRole('checkbox'))).toBeFalsy();
});

describe('shown', () => {
it('should not call onChange when clicked', async () => {
const onChange = jest.fn();
const { queryByRole } = render(
<ShowHideToggle
id="example"
ariaLabelHidden="hidden"
ariaLabelShown="shown"
shown
disabled
onChange={onChange}
/>,
);
await userEvent.click(queryByRole('checkbox'));

expect(onChange).not.toHaveBeenCalled();
});

it('should not call onChange on space', async () => {
const onChange = jest.fn();
const { queryByRole } = render(
<ShowHideToggle
id="example"
ariaLabelHidden="hidden"
ariaLabelShown="shown"
shown
disabled
onChange={onChange}
/>,
);
queryByRole('checkbox').focus();
await userEvent.keyboard('[Space]');

expect(onChange).not.toHaveBeenCalled();
});
});

describe('hidden', () => {
it('should not call onChange when clicked', async () => {
const onChange = jest.fn();
const { queryByRole } = render(
<ShowHideToggle
id="example"
ariaLabelHidden="hidden"
ariaLabelShown="shown"
shown={false}
disabled
onChange={onChange}
/>,
);
await userEvent.click(queryByRole('checkbox'));

expect(onChange).not.toHaveBeenCalled();
});

it('should not call onChange on space', async () => {
const onChange = jest.fn();
const { queryByRole } = render(
<ShowHideToggle
id="example"
ariaLabelHidden="hidden"
ariaLabelShown="shown"
shown={false}
disabled
onChange={onChange}
/>,
);
queryByRole('checkbox').focus();
await userEvent.keyboard('[Space]');

expect(onChange).not.toHaveBeenCalled();
});
});
});
});
8 changes: 7 additions & 1 deletion ui/components/ui/text-field/text-field.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ const themeToInputProps = {
};

const TextField = ({
'data-testid': dataTestId,
error,
classes,
theme,
Expand All @@ -216,14 +217,15 @@ const TextField = ({
autoComplete,
});

if (onPaste) {
if (onPaste || dataTestId) {
if (!inputProps.InputProps) {
inputProps.InputProps = {};
}
if (!inputProps.InputProps.inputProps) {
inputProps.InputProps.inputProps = {};
}
inputProps.InputProps.inputProps.onPaste = onPaste;
inputProps.InputProps.inputProps['data-testid'] = dataTestId;
}

return (
Expand All @@ -243,6 +245,10 @@ TextField.defaultProps = {
};

TextField.propTypes = {
/**
* A test ID that gets set on the input element
*/
'data-testid': PropTypes.string,
/**
* Show error message
*/
Expand Down
1 change: 1 addition & 0 deletions ui/components/ui/ui-components.scss
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
@import 'radio-group/index';
@import 'readonly-input/index';
@import 'sender-to-recipient/index';
@import 'show-hide-toggle/index.scss';
@import 'snackbar/index';
@import 'site-origin/index';
@import 'slider/index';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
exports[`SearchableItemList renders the component with initial props 1`] = `
<div
class="MuiFormControl-root MuiTextField-root searchable-item-list__search MuiFormControl-fullWidth"
data-testid="search-list-items"
>
<div
class="MuiInputBase-root MuiInput-root TextField-inputRoot-12 MuiInputBase-fullWidth MuiInput-fullWidth Mui-focused Mui-focused TextField-inputFocused-11 MuiInputBase-formControl MuiInput-formControl MuiInputBase-adornedStart"
Expand All @@ -23,6 +22,7 @@ exports[`SearchableItemList renders the component with initial props 1`] = `
aria-invalid="false"
autocomplete="off"
class="MuiInputBase-input MuiInput-input MuiInputBase-inputAdornedStart"
data-testid="search-list-items"
dir="auto"
type="text"
value=""
Expand Down
63 changes: 15 additions & 48 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2707,46 +2707,6 @@
web3 "^0.20.7"
web3-provider-engine "^16.0.3"

"@metamask/controllers@^26.0.0":
version "26.0.0"
resolved "https://registry.yarnpkg.com/@metamask/controllers/-/controllers-26.0.0.tgz#3df4a3071ffb26d357ba99f288d52fb9d913c35a"
integrity sha512-iAWDoP/omxGzPfYyBFRNPJ32zcYvZHnUhIM2LyWoCwQj9ZYC1qh+dDX6I0O5jEeQcBrEb+Nl6AcnwHKVdEUz5Q==
dependencies:
"@ethereumjs/common" "^2.3.1"
"@ethereumjs/tx" "^3.2.1"
"@metamask/contract-metadata" "^1.31.0"
"@metamask/metamask-eth-abis" "3.0.0"
"@metamask/types" "^1.1.0"
"@types/uuid" "^8.3.0"
abort-controller "^3.0.0"
async-mutex "^0.2.6"
babel-runtime "^6.26.0"
deep-freeze-strict "^1.1.1"
eth-ens-namehash "^2.0.8"
eth-json-rpc-infura "^5.1.0"
eth-keyring-controller "^6.2.1"
eth-method-registry "1.1.0"
eth-phishing-detect "^1.1.14"
eth-query "^2.1.2"
eth-rpc-errors "^4.0.0"
eth-sig-util "^3.0.0"
ethereumjs-util "^7.0.10"
ethereumjs-wallet "^1.0.1"
ethers "^5.4.1"
ethjs-unit "^0.1.6"
fast-deep-equal "^3.1.3"
immer "^9.0.6"
isomorphic-fetch "^3.0.0"
json-rpc-engine "^6.1.0"
jsonschema "^1.2.4"
multiformats "^9.5.2"
nanoid "^3.1.31"
punycode "^2.1.1"
single-call-balance-checker-abi "^1.0.0"
uuid "^8.3.2"
web3 "^0.20.7"
web3-provider-engine "^16.0.3"

"@metamask/controllers@^27.0.0":
version "27.0.0"
resolved "https://registry.yarnpkg.com/@metamask/controllers/-/controllers-27.0.0.tgz#23fb24960880047635a7e0b226375b843f385ad1"
Expand Down Expand Up @@ -4176,6 +4136,13 @@
"@babel/runtime" "^7.10.3"
"@testing-library/dom" "^7.17.1"

"@testing-library/user-event@^14.0.0-beta.12":
version "14.0.0-beta.12"
resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.0.0-beta.12.tgz#8df662578e49371fd80d5923d92f3c378f7dd927"
integrity sha512-vFZQBBzO14bJseKAS78Ae/coSuJbgrs7ywRZw88Hc52Le8RJGehdxR4w25Oj7QVNpZZpz0R6q1zMVdYGtPbd2A==
dependencies:
"@babel/runtime" "^7.12.5"

"@tootallnate/once@1":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
Expand Down Expand Up @@ -7957,10 +7924,10 @@ chrome-trace-event@^1.0.2:
dependencies:
tslib "^1.9.0"

chromedriver@^98.0.0:
version "98.0.0"
resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-98.0.0.tgz#b2c3c1941fad4cdfadad5d4c46923e02f089fd30"
integrity sha512-Oi6Th5teK+VI4nti+423/dFkENYHEMOdUvqwJHzOaNwXqLwZ8FuSaKBybgALCctGapwJbd+tmPv3qSd6tUUIHQ==
chromedriver@^99.0.0:
version "99.0.0"
resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-99.0.0.tgz#fbfcc7e74991dd50962e7dd456d78eaf49f56774"
integrity sha512-pyB+5LuyZdb7EBPL3i5D5yucZUD+SlkdiUtmpjaEnLd9zAXp+SvD/hP5xF4l/ZmWvUo/1ZLxAI1YBdhazGTpgA==
dependencies:
"@testim/chrome-version" "^1.1.2"
axios "^0.24.0"
Expand Down Expand Up @@ -19759,10 +19726,10 @@ node-fetch@^2.3.0, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@~2.6.1:
dependencies:
whatwg-url "^5.0.0"

node-forge@^0.7.1, node-forge@^0.7.5, node-forge@^1.0.0, node-forge@^1.2.1, node-forge@~0.7.6:
version "1.2.1"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.2.1.tgz#82794919071ef2eb5c509293325cec8afd0fd53c"
integrity sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w==
node-forge@^0.7.1, node-forge@^0.7.5, node-forge@^1.2.1, node-forge@^1.3.0, node-forge@~0.7.6:
version "1.3.0"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.0.tgz#37a874ea723855f37db091e6c186e5b67a01d4b2"
integrity sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA==

node-gyp-build@4.3.0, node-gyp-build@^4.2.0, node-gyp-build@^4.2.2, node-gyp-build@^4.2.3, node-gyp-build@^4.3.0:
version "4.3.0"
Expand Down