From bea907e43757dafcc05b9a5bf3a43ae0881d5eb5 Mon Sep 17 00:00:00 2001 From: VSaric <92527393+VSaric@users.noreply.github.com> Date: Mon, 7 Mar 2022 16:35:03 +0100 Subject: [PATCH] Create password page (#13792) --- app/_locales/en/messages.json | 17 +- lavamoat/build-system/policy.json | 201 ++++++++++++++++++ package.json | 3 +- ui/components/ui/form-field/form-field.js | 28 +++ ui/helpers/constants/zendesk-url.js | 2 + .../create-password/create-password.js | 55 ++++- .../create-password/index.scss | 12 ++ yarn.lock | 5 + 8 files changed, 311 insertions(+), 12 deletions(-) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index ebe1cbc07..6b1a95652 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1983,7 +1983,7 @@ "message": "“$1” was successfully added!" }, "newPassword": { - "message": "New password (min 8 chars)" + "message": "New password (8 characters min)" }, "newToMetaMask": { "message": "New to MetaMask?" @@ -2277,11 +2277,18 @@ "passwordSetupDetails": { "message": "This password will unlock your MetaMask wallet only on this device. MetaMask can not recover this password." }, + "passwordStrength": { + "message": "Password strength: $1", + "description": "Return password strength to the user when user wants to create password." + }, + "passwordStrengthDescription": { + "message": "A strong password can improve the security of your wallet should your device be stolen or compromised." + }, "passwordTermsWarning": { "message": "I understand that MetaMask cannot recover this password for me. $1" }, "passwordsDontMatch": { - "message": "Passwords Don't Match" + "message": "Passwords don't match" }, "pastePrivateKey": { "message": "Enter your private key string here:", @@ -2948,6 +2955,9 @@ "storePhrase": { "message": "Store this phrase in a password manager like 1Password." }, + "strong": { + "message": "Strong" + }, "stxAreHere": { "message": "Smart Transactions are here!" }, @@ -3772,6 +3782,9 @@ "walletCreationSuccessTitle": { "message": "Wallet creation successful" }, + "weak": { + "message": "Weak" + }, "web3ShimUsageNotification": { "message": "We noticed that the current website tried to use the removed window.web3 API. If the site appears to be broken, please click $1 for more information.", "description": "$1 is a clickable link." diff --git a/lavamoat/build-system/policy.json b/lavamoat/build-system/policy.json index e95acc4d6..7d6167325 100644 --- a/lavamoat/build-system/policy.json +++ b/lavamoat/build-system/policy.json @@ -1052,6 +1052,16 @@ "buffer-equal": true } }, + "are-we-there-yet": { + "builtin": { + "events.EventEmitter": true, + "util.inherits": true + }, + "packages": { + "delegates": true, + "readable-stream": true + } + }, "arr-diff": { "packages": { "arr-flatten": true, @@ -1460,6 +1470,7 @@ "anymatch": true, "async-each": true, "braces": true, + "fsevents": true, "glob-parent": true, "inherits": true, "is-binary-path": true, @@ -1726,6 +1737,16 @@ "through2": true } }, + "detect-libc": { + "builtin": { + "child_process.spawnSync": true, + "fs.readdirSync": true, + "os.platform": true + }, + "globals": { + "process.env": true + } + }, "detective": { "packages": { "acorn-node": true, @@ -2429,6 +2450,45 @@ "process.version": true } }, + "fsevents": { + "builtin": { + "events.EventEmitter": true, + "fs.stat": true, + "path.join": true, + "util.inherits": true + }, + "globals": { + "__dirname": true, + "process.nextTick": true, + "process.platform": true, + "setImmediate": true + }, + "native": true, + "packages": { + "node-pre-gyp": true + } + }, + "gauge": { + "builtin": { + "util.format": true + }, + "globals": { + "clearInterval": true, + "process": true, + "setImmediate": true, + "setInterval": true + }, + "packages": { + "aproba": true, + "console-control-strings": true, + "has-unicode": true, + "object-assign": true, + "signal-exit": true, + "string-width": true, + "strip-ansi": true, + "wide-align": true + } + }, "get-assigned-identifiers": { "builtin": { "assert.equal": true @@ -2807,6 +2867,16 @@ "process.argv": true } }, + "has-unicode": { + "builtin": { + "os.type": true + }, + "globals": { + "process.env.LANG": true, + "process.env.LC_ALL": true, + "process.env.LC_CTYPE": true + } + }, "has-value": { "packages": { "get-value": true, @@ -2978,6 +3048,11 @@ "is-plain-object": true } }, + "is-fullwidth-code-point": { + "packages": { + "number-is-nan": true + } + }, "is-glob": { "packages": { "is-extglob": true @@ -3508,6 +3583,56 @@ "setTimeout": true } }, + "node-pre-gyp": { + "builtin": { + "events.EventEmitter": true, + "fs.existsSync": true, + "fs.readFileSync": true, + "fs.renameSync": true, + "path.dirname": true, + "path.existsSync": true, + "path.join": true, + "path.resolve": true, + "url.parse": true, + "url.resolve": true, + "util.inherits": true + }, + "globals": { + "__dirname": true, + "console.log": true, + "process.arch": true, + "process.cwd": true, + "process.env": true, + "process.platform": true, + "process.version.substr": true, + "process.versions": true + }, + "packages": { + "detect-libc": true, + "nopt": true, + "npmlog": true, + "rimraf": true, + "semver": true + } + }, + "nopt": { + "builtin": { + "path": true, + "stream.Stream": true, + "url": true + }, + "globals": { + "console": true, + "process.argv": true, + "process.env.DEBUG_NOPT": true, + "process.env.NOPT_DEBUG": true, + "process.platform": true + }, + "packages": { + "abbrev": true, + "osenv": true + } + }, "normalize-package-data": { "builtin": { "url.parse": true, @@ -3535,6 +3660,22 @@ "once": true } }, + "npmlog": { + "builtin": { + "events.EventEmitter": true, + "util": true + }, + "globals": { + "process.nextTick": true, + "process.stderr": true + }, + "packages": { + "are-we-there-yet": true, + "console-control-strings": true, + "gauge": true, + "set-blocking": true + } + }, "object-copy": { "packages": { "copy-descriptor": true, @@ -3616,6 +3757,54 @@ "readable-stream": true } }, + "os-homedir": { + "builtin": { + "os.homedir": true + }, + "globals": { + "process.env": true, + "process.getuid": true, + "process.platform": true + } + }, + "os-tmpdir": { + "globals": { + "process.env.SystemRoot": true, + "process.env.TEMP": true, + "process.env.TMP": true, + "process.env.TMPDIR": true, + "process.env.windir": true, + "process.platform": true + } + }, + "osenv": { + "builtin": { + "child_process.exec": true, + "path": true + }, + "globals": { + "process.env.COMPUTERNAME": true, + "process.env.ComSpec": true, + "process.env.EDITOR": true, + "process.env.HOSTNAME": true, + "process.env.PATH": true, + "process.env.PROMPT": true, + "process.env.PS1": true, + "process.env.Path": true, + "process.env.SHELL": true, + "process.env.USER": true, + "process.env.USERDOMAIN": true, + "process.env.USERNAME": true, + "process.env.VISUAL": true, + "process.env.path": true, + "process.nextTick": true, + "process.platform": true + }, + "packages": { + "os-homedir": true, + "os-tmpdir": true + } + }, "p-limit": { "packages": { "p-try": true @@ -4325,6 +4514,12 @@ "lru-cache": true } }, + "set-blocking": { + "globals": { + "process.stderr": true, + "process.stdout": true + } + }, "set-value": { "packages": { "extend-shallow": true, @@ -4588,6 +4783,7 @@ }, "string-width": { "packages": { + "code-point-at": true, "emoji-regex": true, "is-fullwidth-code-point": true, "strip-ansi": true @@ -5240,6 +5436,11 @@ "isexe": true } }, + "wide-align": { + "packages": { + "string-width": true + } + }, "write": { "builtin": { "fs.createWriteStream": true, diff --git a/package.json b/package.json index c5befd776..d34aff663 100644 --- a/package.json +++ b/package.json @@ -221,7 +221,8 @@ "uuid": "^8.3.2", "valid-url": "^1.0.9", "web3": "^0.20.7", - "web3-stream-provider": "^4.0.0" + "web3-stream-provider": "^4.0.0", + "zxcvbn": "^4.4.2" }, "devDependencies": { "@babel/code-frame": "^7.12.13", diff --git a/ui/components/ui/form-field/form-field.js b/ui/components/ui/form-field/form-field.js index c97353ff9..43788d828 100644 --- a/ui/components/ui/form-field/form-field.js +++ b/ui/components/ui/form-field/form-field.js @@ -32,6 +32,8 @@ export default function FormField({ disabled, placeholder, warning, + passwordStrength, + passwordStrengthText, }) { return (
)} + {passwordStrength && ( + + {passwordStrength} + + )} + {passwordStrengthText && ( + + {passwordStrengthText} + + )}
); @@ -192,6 +212,14 @@ FormField.propTypes = { * Set the placeholder text for the input field */ placeholder: PropTypes.string, + /** + * Show password strength according to the score + */ + passwordStrength: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + /** + * Show password strength description + */ + passwordStrengthText: PropTypes.string, }; FormField.defaultProps = { diff --git a/ui/helpers/constants/zendesk-url.js b/ui/helpers/constants/zendesk-url.js index 414603526..010d54cfb 100644 --- a/ui/helpers/constants/zendesk-url.js +++ b/ui/helpers/constants/zendesk-url.js @@ -9,6 +9,8 @@ const ZENDESK_URLS = { 'https://metamask.zendesk.com/hc/en-us/articles/4403988839451', SECRET_RECOVERY_PHRASE: 'https://metamask.zendesk.com/hc/en-us/articles/360060826432-What-is-a-Secret-Recovery-Phrase-and-how-to-keep-your-crypto-wallet-secure', + PASSWORD_ARTICLE: + 'https://metamask.zendesk.com/hc/en-us/articles/4404722782107', }; export default ZENDESK_URLS; diff --git a/ui/pages/onboarding-flow/create-password/create-password.js b/ui/pages/onboarding-flow/create-password/create-password.js index fcdbca69a..26a35c026 100644 --- a/ui/pages/onboarding-flow/create-password/create-password.js +++ b/ui/pages/onboarding-flow/create-password/create-password.js @@ -1,6 +1,7 @@ import React, { useState, useMemo } from 'react'; import PropTypes from 'prop-types'; import { useHistory } from 'react-router-dom'; +import zxcvbn from 'zxcvbn'; import { useNewMetricEvent } from '../../../hooks/useMetricEvent'; import { useI18nContext } from '../../../hooks/useI18nContext'; import Button from '../../../components/ui/button'; @@ -25,6 +26,7 @@ import { TwoStepProgressBar, twoStepStages, } from '../../../components/app/step-progress-bar'; +import ZENDESK_URLS from '../../../helpers/constants/zendesk-url'; export default function CreatePassword({ createNewAccount, @@ -35,6 +37,8 @@ export default function CreatePassword({ const [confirmPassword, setConfirmPassword] = useState(''); const [password, setPassword] = useState(''); const [passwordError, setPasswordError] = useState(''); + const [passwordStrength, setPasswordStrength] = useState(''); + const [passwordStrengthText, setPasswordStrengthText] = useState(''); const [confirmPasswordError, setConfirmPasswordError] = useState(''); const [termsChecked, setTermsChecked] = useState(false); const [showPassword, setShowPassword] = useState(false); @@ -57,19 +61,51 @@ export default function CreatePassword({ return !passwordError && !confirmPasswordError; }, [password, confirmPassword, passwordError, confirmPasswordError]); - const handlePasswordChange = (passwordInput) => { - let error = ''; - let confirmError = ''; - if (passwordInput && passwordInput.length < 8) { - error = t('passwordNotLongEnough'); + const getPasswordStrengthLabel = (score, translation) => { + if (score >= 4) { + return { + className: 'create-password__strong', + text: translation('strong'), + description: '', + }; + } else if (score === 3) { + return { + className: 'create-password__average', + text: translation('average'), + description: t('passwordStrengthDescription'), + }; } + return { + className: 'create-password__weak', + text: translation('weak'), + description: t('passwordStrengthDescription'), + }; + }; + + const handlePasswordChange = (passwordInput) => { + let confirmError = ''; + const passwordEvaluation = zxcvbn(passwordInput); + const passwordStrengthLabel = getPasswordStrengthLabel( + passwordEvaluation.score, + t, + ); + const passwordStrengthDescription = passwordStrengthLabel.description; + const passwordStrengthInput = t('passwordStrength', [ + + {passwordStrengthLabel.text} + , + ]); if (confirmPassword && passwordInput !== confirmPassword) { confirmError = t('passwordsDontMatch'); } setPassword(passwordInput); - setPasswordError(error); + setPasswordStrength(passwordStrengthInput); + setPasswordStrengthText(passwordStrengthDescription); setConfirmPasswordError(confirmError); }; @@ -133,7 +169,8 @@ export default function CreatePassword({ e.stopPropagation()} key="create-password__link-text" - href="https://metamask.io/terms.html" + href={ZENDESK_URLS.PASSWORD_ARTICLE} target="_blank" rel="noopener noreferrer" > - {t('learnMore')} + {t('learnMoreUpperCase')} , ])} diff --git a/ui/pages/onboarding-flow/create-password/index.scss b/ui/pages/onboarding-flow/create-password/index.scss index cc12200ae..0979fde43 100644 --- a/ui/pages/onboarding-flow/create-password/index.scss +++ b/ui/pages/onboarding-flow/create-password/index.scss @@ -1,4 +1,16 @@ .create-password { + &__weak { + color: var(--error-1); + } + + &__average { + color: var(--secondary-3);; + } + + &__strong { + color: var(--success-3); + } + &__wrapper { display: flex; justify-content: center; diff --git a/yarn.lock b/yarn.lock index 259102ceb..84f580cb9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -28349,3 +28349,8 @@ zwitch@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920" integrity sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw== + +zxcvbn@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/zxcvbn/-/zxcvbn-4.4.2.tgz#28ec17cf09743edcab056ddd8b1b06262cc73c30" + integrity sha1-KOwXzwl0PtyrBW3dixsGJizHPDA=