diff options
author | Alexander Tseung <alextsg@gmail.com> | 2018-05-13 11:53:40 +0800 |
---|---|---|
committer | Alexander Tseung <alextsg@gmail.com> | 2018-05-14 12:46:12 +0800 |
commit | 0bcfbc15446b01b3b87233715cd3ead42d2730e4 (patch) | |
tree | 8b39abffed694538464ff00c2471fdc0df25ecd8 | |
parent | 6bd1b21d3b8f74c44214f814c3fe1c8770ab9e2d (diff) | |
download | tangerine-wallet-browser-0bcfbc15446b01b3b87233715cd3ead42d2730e4.tar tangerine-wallet-browser-0bcfbc15446b01b3b87233715cd3ead42d2730e4.tar.gz tangerine-wallet-browser-0bcfbc15446b01b3b87233715cd3ead42d2730e4.tar.bz2 tangerine-wallet-browser-0bcfbc15446b01b3b87233715cd3ead42d2730e4.tar.lz tangerine-wallet-browser-0bcfbc15446b01b3b87233715cd3ead42d2730e4.tar.xz tangerine-wallet-browser-0bcfbc15446b01b3b87233715cd3ead42d2730e4.tar.zst tangerine-wallet-browser-0bcfbc15446b01b3b87233715cd3ead42d2730e4.zip |
Add error message when passwords don't match in first time flow. Change input field styling in first time flow
-rw-r--r-- | app/_locales/en/messages.json | 2 | ||||
-rw-r--r-- | mascara/src/app/first-time/create-password-screen.js | 133 | ||||
-rw-r--r-- | mascara/src/app/first-time/import-seed-phrase-screen.js | 146 | ||||
-rw-r--r-- | mascara/src/app/first-time/index.css | 33 | ||||
-rw-r--r-- | test/integration/lib/mascara-first-time.js | 29 | ||||
-rw-r--r-- | test/screens/new-ui.js | 4 | ||||
-rw-r--r-- | ui/app/components/text-field/text-field.component.js | 5 | ||||
-rw-r--r-- | ui/app/css/itcss/components/welcome-screen.scss | 97 |
8 files changed, 216 insertions, 233 deletions
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 214355589..90beda418 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -724,7 +724,7 @@ "message": "New Password (min 8 chars)" }, "seedPhraseReq": { - "message": "seed phrases are 12 words long" + "message": "Seed phrases are 12 words long" }, "select": { "message": "Select" diff --git a/mascara/src/app/first-time/create-password-screen.js b/mascara/src/app/first-time/create-password-screen.js index 6ec05e11d..99d210ed1 100644 --- a/mascara/src/app/first-time/create-password-screen.js +++ b/mascara/src/app/first-time/create-password-screen.js @@ -13,8 +13,13 @@ import { INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE, INITIALIZE_NOTICE_ROUTE, } from '../../../../ui/app/routes' +import TextField from '../../../../ui/app/components/text-field' class CreatePasswordScreen extends Component { + static contextTypes = { + t: PropTypes.func, + } + static propTypes = { isLoading: PropTypes.bool.isRequired, createAccount: PropTypes.func.isRequired, @@ -27,6 +32,8 @@ class CreatePasswordScreen extends Component { state = { password: '', confirmPassword: '', + passwordError: null, + confirmPasswordError: null, } constructor (props) { @@ -69,82 +76,37 @@ class CreatePasswordScreen extends Component { .then(() => history.push(INITIALIZE_UNIQUE_IMAGE_ROUTE)) } - renderFields () { - const { isMascara, history } = this.props + handlePasswordChange (password) { + const { confirmPassword } = this.state + let confirmPasswordError = null + let passwordError = null - return ( - <div className={classnames({ 'first-view-main-wrapper': !isMascara })}> - <div className={classnames({ - 'first-view-main': !isMascara, - 'first-view-main__mascara': isMascara, - })}> - {isMascara && <div className="mascara-info first-view-phone-invisible"> - <Mascot - animationEventEmitter={this.animationEventEmitter} - width="225" - height="225" - /> - <div className="info"> - MetaMask is a secure identity vault for Ethereum. - </div> - <div className="info"> - It allows you to hold ether & tokens, and interact with decentralized applications. - </div> - </div>} - <div className="create-password"> - <div className="create-password__title"> - Create Password - </div> - <input - className="first-time-flow__input" - type="password" - placeholder="New Password (min 8 characters)" - onChange={e => this.setState({password: e.target.value})} - /> - <input - className="first-time-flow__input create-password__confirm-input" - type="password" - placeholder="Confirm Password" - onChange={e => this.setState({confirmPassword: e.target.value})} - /> - <button - className="first-time-flow__button" - disabled={!this.isValid()} - onClick={this.createAccount} - > - Create - </button> - <a - href="" - className="first-time-flow__link create-password__import-link" - onClick={e => { - e.preventDefault() - history.push(INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE) - }} - > - Import with seed phrase - </a> - { /* } - <a - href="" - className="first-time-flow__link create-password__import-link" - onClick={e => { - e.preventDefault() - history.push(INITIALIZE_IMPORT_ACCOUNT_ROUTE) - }} - > - Import an account - </a> - { */ } - <Breadcrumbs total={3} currentIndex={0} /> - </div> - </div> - </div> - ) + if (password && password.length < 8) { + passwordError = this.context.t('passwordNotLongEnough') + } + + if (confirmPassword && password !== confirmPassword) { + confirmPasswordError = this.context.t('passwordsDontMatch') + } + + this.setState({ password, passwordError, confirmPasswordError }) + } + + handleConfirmPasswordChange (confirmPassword) { + const { password } = this.state + let confirmPasswordError = null + + if (password !== confirmPassword) { + confirmPasswordError = this.context.t('passwordsDontMatch') + } + + this.setState({ confirmPassword, confirmPasswordError }) } render () { const { history, isMascara } = this.props + const { passwordError, confirmPasswordError } = this.state + const { t } = this.context return ( <div className={classnames({ 'first-view-main-wrapper': !isMascara })}> @@ -169,17 +131,30 @@ class CreatePasswordScreen extends Component { <div className="create-password__title"> Create Password </div> - <input - className="first-time-flow__input" + <TextField + id="create-password" + label={t('newPassword')} type="password" - placeholder="New Password (min 8 characters)" - onChange={e => this.setState({password: e.target.value})} + className="first-time-flow__input" + value={this.state.password} + onChange={event => this.handlePasswordChange(event.target.value)} + error={passwordError} + autoFocus + autoComplete="new-password" + margin="normal" + fullWidth /> - <input - className="first-time-flow__input create-password__confirm-input" + <TextField + id="confirm-password" + label={t('confirmPassword')} type="password" - placeholder="Confirm Password" - onChange={e => this.setState({confirmPassword: e.target.value})} + className="first-time-flow__input" + value={this.state.confirmPassword} + onChange={event => this.handleConfirmPasswordChange(event.target.value)} + error={confirmPasswordError} + autoComplete="confirm-password" + margin="normal" + fullWidth /> <button className="first-time-flow__button" diff --git a/mascara/src/app/first-time/import-seed-phrase-screen.js b/mascara/src/app/first-time/import-seed-phrase-screen.js index 5834919de..4fda2bbc1 100644 --- a/mascara/src/app/first-time/import-seed-phrase-screen.js +++ b/mascara/src/app/first-time/import-seed-phrase-screen.js @@ -1,29 +1,33 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import {connect} from 'react-redux' -import classnames from 'classnames' import { createNewVaultAndRestore, - hideWarning, - displayWarning, unMarkPasswordForgotten, } from '../../../../ui/app/actions' -import { DEFAULT_ROUTE, INITIALIZE_NOTICE_ROUTE } from '../../../../ui/app/routes' +import { INITIALIZE_NOTICE_ROUTE } from '../../../../ui/app/routes' +import TextField from '../../../../ui/app/components/text-field' class ImportSeedPhraseScreen extends Component { + static contextTypes = { + t: PropTypes.func, + } + static propTypes = { warning: PropTypes.string, createNewVaultAndRestore: PropTypes.func.isRequired, - hideWarning: PropTypes.func.isRequired, - displayWarning: PropTypes.func, leaveImportSeedScreenState: PropTypes.func, history: PropTypes.object, + isLoading: PropTypes.bool, }; state = { seedPhrase: '', password: '', confirmPassword: '', + seedPhraseError: null, + passwordError: null, + confirmPasswordError: null, } parseSeedPhrase = (seedPhrase) => { @@ -32,39 +36,47 @@ class ImportSeedPhraseScreen extends Component { .join(' ') } - onChange = ({ seedPhrase, password, confirmPassword }) => { - const { - password: prevPassword, - confirmPassword: prevConfirmPassword, - } = this.state - const { displayWarning, hideWarning } = this.props - - let warning = null + handleSeedPhraseChange (seedPhrase) { + let seedPhraseError = null if (seedPhrase && this.parseSeedPhrase(seedPhrase).split(' ').length !== 12) { - warning = 'Seed Phrases are 12 words long' - } else if (password && password.length < 8) { - warning = 'Passwords require a mimimum length of 8' - } else if ((password || prevPassword) !== (confirmPassword || prevConfirmPassword)) { - warning = 'Confirmed password does not match' + seedPhraseError = this.context.t('seedPhraseReq') } - if (warning) { - displayWarning(warning) - } else { - hideWarning() + this.setState({ seedPhrase, seedPhraseError }) + } + + handlePasswordChange (password) { + const { confirmPassword } = this.state + let confirmPasswordError = null + let passwordError = null + + if (password && password.length < 8) { + passwordError = this.context.t('passwordNotLongEnough') + } + + if (confirmPassword && password !== confirmPassword) { + confirmPasswordError = this.context.t('passwordsDontMatch') + } + + this.setState({ password, passwordError, confirmPasswordError }) + } + + handleConfirmPasswordChange (confirmPassword) { + const { password } = this.state + let confirmPasswordError = null + + if (password !== confirmPassword) { + confirmPasswordError = this.context.t('passwordsDontMatch') } - seedPhrase && this.setState({ seedPhrase }) - password && this.setState({ password }) - confirmPassword && this.setState({ confirmPassword }) + this.setState({ confirmPassword, confirmPasswordError }) } onClick = () => { const { password, seedPhrase } = this.state const { createNewVaultAndRestore, - displayWarning, leaveImportSeedScreenState, history, } = this.props @@ -74,10 +86,23 @@ class ImportSeedPhraseScreen extends Component { .then(() => history.push(INITIALIZE_NOTICE_ROUTE)) } + hasError () { + const { passwordError, confirmPasswordError, seedPhraseError } = this.state + return passwordError || confirmPasswordError || seedPhraseError + } + render () { - const { seedPhrase, password, confirmPassword } = this.state - const { warning, isLoading } = this.props - const importDisabled = warning || !seedPhrase || !password || !confirmPassword || isLoading + const { + seedPhrase, + password, + confirmPassword, + seedPhraseError, + passwordError, + confirmPasswordError, + } = this.state + const { t } = this.context + const { isLoading } = this.props + const disabled = !seedPhrase || !password || !confirmPassword || isLoading || this.hasError() return ( <div className="first-view-main-wrapper"> @@ -103,45 +128,40 @@ class ImportSeedPhraseScreen extends Component { <label className="import-account__input-label">Wallet Seed</label> <textarea className="import-account__secret-phrase" - onChange={e => this.onChange({seedPhrase: e.target.value})} + onChange={e => this.handleSeedPhraseChange(e.target.value)} value={this.state.seedPhrase} placeholder="Separate each word with a single space" /> </div> - <span - className="error" - > - {this.props.warning} + <span className="error"> + { seedPhraseError } </span> - <div className="import-account__input-wrapper"> - <label className="import-account__input-label">New Password</label> - <input - className="first-time-flow__input" - type="password" - placeholder="New Password (min 8 characters)" - onChange={e => this.onChange({password: e.target.value})} - /> - </div> - <div className="import-account__input-wrapper"> - <label - className={classnames('import-account__input-label', { - 'import-account__input-label__disabled': password.length < 8, - })} - >Confirm Password</label> - <input - className={classnames('first-time-flow__input', { - 'first-time-flow__input__disabled': password.length < 8, - })} - type="password" - placeholder="Confirm Password" - onChange={e => this.onChange({confirmPassword: e.target.value})} - disabled={password.length < 8} - /> - </div> + <TextField + id="password" + label={t('newPassword')} + type="password" + className="first-time-flow__input" + value={this.state.password} + onChange={event => this.handlePasswordChange(event.target.value)} + error={passwordError} + autoComplete="new-password" + margin="normal" + /> + <TextField + id="confirm-password" + label={t('confirmPassword')} + type="password" + className="first-time-flow__input" + value={this.state.confirmPassword} + onChange={event => this.handleConfirmPasswordChange(event.target.value)} + error={confirmPasswordError} + autoComplete="confirm-password" + margin="normal" + /> <button className="first-time-flow__button" - onClick={() => !importDisabled && this.onClick()} - disabled={importDisabled} + onClick={() => !disabled && this.onClick()} + disabled={disabled} > Import </button> @@ -159,7 +179,5 @@ export default connect( dispatch(unMarkPasswordForgotten()) }, createNewVaultAndRestore: (pw, seed) => dispatch(createNewVaultAndRestore(pw, seed)), - displayWarning: (warning) => dispatch(displayWarning(warning)), - hideWarning: () => dispatch(hideWarning()), }) )(ImportSeedPhraseScreen) diff --git a/mascara/src/app/first-time/index.css b/mascara/src/app/first-time/index.css index dffc21017..25e60b84a 100644 --- a/mascara/src/app/first-time/index.css +++ b/mascara/src/app/first-time/index.css @@ -174,10 +174,7 @@ } .first-time-flow__input { - width: initial !important; - font-size: 14px !important; - line-height: 18px !important; - padding: 12px !important; + width: 100%; } .tou__body { @@ -248,7 +245,7 @@ } .create-password__confirm-input { - margin-top: 15px; + margin-top: 16px; } .create-password__import-link { @@ -520,10 +517,6 @@ button.backup-phrase__confirm-seed-option:hover { margin-top: 30px; } -.first-time-flow__input--error { - border: 1px solid #FF001F !important; -} - .import-account__input-error-message { margin-top: 10px; width: 422px; @@ -544,7 +537,13 @@ button.backup-phrase__confirm-seed-option:hover { } .import-account__input { - width: 325px !important; + width: 350px; +} + +@media only screen and (max-width: 575px) { + .import-account__input { + width: 100%; + } } .import-account__file-input { @@ -681,20 +680,6 @@ button.backup-phrase__confirm-seed-option:hover { .first-time-flow__input { width: 350px; - font-size: 18px; - line-height: 24px; - padding: 15px; - border: 1px solid #CDCDCD; - background-color: #FFFFFF; -} - -.first-time-flow__input__disabled { - opacity: 0.5; -} - -.first-time-flow__input::placeholder { - color: #9B9B9B; - font-weight: 200; } .first-time-flow__button { diff --git a/test/integration/lib/mascara-first-time.js b/test/integration/lib/mascara-first-time.js index d86703277..f43a30c74 100644 --- a/test/integration/lib/mascara-first-time.js +++ b/test/integration/lib/mascara-first-time.js @@ -1,5 +1,4 @@ const PASSWORD = 'password123' -const reactTriggerChange = require('react-trigger-change') const { timeout, findAsync, @@ -11,6 +10,11 @@ async function runFirstTimeUsageTest (assert, done) { const app = await queryAsync($, '#app-content') + // Used to set values on TextField input component + const nativeInputValueSetter = Object.getOwnPropertyDescriptor( + window.HTMLInputElement.prototype, 'value' + ).set + await skipNotices(app) const welcomeButton = (await findAsync(app, '.welcome-screen__button'))[0] @@ -21,12 +25,14 @@ async function runFirstTimeUsageTest (assert, done) { assert.equal(title, 'Create Password', 'create password screen') // enter password - const pwBox = (await findAsync(app, '.first-time-flow__input'))[0] - const confBox = (await findAsync(app, '.first-time-flow__input'))[1] - pwBox.value = PASSWORD - confBox.value = PASSWORD - reactTriggerChange(pwBox) - reactTriggerChange(confBox) + const pwBox = (await findAsync(app, '#create-password'))[0] + const confBox = (await findAsync(app, '#confirm-password'))[0] + + nativeInputValueSetter.call(pwBox, PASSWORD) + pwBox.dispatchEvent(new Event('input', { bubbles: true})) + + nativeInputValueSetter.call(confBox, PASSWORD) + confBox.dispatchEvent(new Event('input', { bubbles: true})) // Create Password const createButton = (await findAsync(app, 'button.first-time-flow__button'))[0] @@ -77,15 +83,8 @@ async function runFirstTimeUsageTest (assert, done) { pwBox2.focus() await timeout(1000) - // Used to set values on TextField input component - const nativeInputValueSetter = Object.getOwnPropertyDescriptor( - window.HTMLInputElement.prototype, 'value' - ).set - nativeInputValueSetter.call(pwBox2, PASSWORD) - - var ev2 = new Event('input', { bubbles: true}) - pwBox2.dispatchEvent(ev2) + pwBox2.dispatchEvent(new Event('input', { bubbles: true})) const createButton2 = (await findAsync(app, 'button[type="submit"]'))[0] createButton2.click() diff --git a/test/screens/new-ui.js b/test/screens/new-ui.js index e176da529..6a8822eb3 100644 --- a/test/screens/new-ui.js +++ b/test/screens/new-ui.js @@ -74,8 +74,8 @@ async function captureAllScreens() { await driver.findElement(By.css('button')).click() await captureLanguageScreenShots('create password') - const passwordBox = await driver.findElement(By.css('input[type=password]:nth-of-type(1)')) - const passwordBoxConfirm = await driver.findElement(By.css('input[type=password]:nth-of-type(2)')) + const passwordBox = await driver.findElement(By.css('input#create-password')) + const passwordBoxConfirm = await driver.findElement(By.css('input#confirm-password')) passwordBox.sendKeys('123456789') passwordBoxConfirm.sendKeys('123456789') await delay(500) diff --git a/ui/app/components/text-field/text-field.component.js b/ui/app/components/text-field/text-field.component.js index 4a02f76d8..6fd3b82b4 100644 --- a/ui/app/components/text-field/text-field.component.js +++ b/ui/app/components/text-field/text-field.component.js @@ -8,6 +8,9 @@ const styles = { '&$cssFocused': { color: '#aeaeae', }, + '&$cssError': { + color: '#aeaeae', + }, fontWeight: '400', color: '#aeaeae', }, @@ -17,6 +20,7 @@ const styles = { backgroundColor: '#f7861c', }, }, + cssError: {}, } const TextField = props => { @@ -30,6 +34,7 @@ const TextField = props => { FormLabelClasses: { root: classes.cssLabel, focused: classes.cssFocused, + error: classes.cssError, }, }} InputProps={{ diff --git a/ui/app/css/itcss/components/welcome-screen.scss b/ui/app/css/itcss/components/welcome-screen.scss index bfd174ad9..af1d67398 100644 --- a/ui/app/css/itcss/components/welcome-screen.scss +++ b/ui/app/css/itcss/components/welcome-screen.scss @@ -1,59 +1,60 @@ .welcome-screen { + display: flex; + flex-flow: column; + justify-content: center; + align-items: center; + font-family: Roboto; + font-weight: 400; + width: 100%; + flex: 1 0 auto; + padding: 70px 0; + background: $white; + + @media screen and (max-width: 575px) { + padding: 0; + } + + &__info { display: flex; flex-flow: column; - justify-content: center; - align-items: center; - font-family: Roboto; - font-weight: 400; width: 100%; - flex: 1 0 auto; - padding: 70px 0; - background: $white; - - @media screen and (max-width: 575px) { - padding: 0; - } - - &__info { - display: flex; - flex-flow: column; - width: 100%; - height: 100%; - align-items: center; - - &__header { - font-size: 1.65em; - margin-bottom: 14px; - - @media screen and (max-width: 575px) { - font-size: 1.5em; - } - } + height: 100%; + align-items: center; + justify-content: center; - &__copy { - font-size: 1em; - width: 400px; - max-width: 90vw; - text-align: center; + &__header { + font-size: 1.65em; + margin-bottom: 14px; - @media screen and (max-width: 575px) { - font-size: 0.9em; - } - } + @media screen and (max-width: 575px) { + font-size: 1.5em; + } } - &__button { - height: 54px; - width: 198px; - box-shadow: 0 2px 4px 0 rgba(0,0,0,0.14); - color: #FFFFFF; - font-size: 20px; - font-weight: 500; - line-height: 26px; + &__copy { + font-size: 1em; + width: 400px; + max-width: 90vw; text-align: center; - text-transform: uppercase; - margin: 35px 0 14px; - transition: 200ms ease-in-out; - background-color: rgba(247, 134, 28, 0.9); + + @media screen and (max-width: 575px) { + font-size: .9em; + } } + } + + &__button { + height: 54px; + width: 198px; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .14); + color: #fff; + font-size: 20px; + font-weight: 500; + line-height: 26px; + text-align: center; + text-transform: uppercase; + margin: 35px 0 14px; + transition: 200ms ease-in-out; + background-color: rgba(247, 134, 28, .9); + } } |