1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 01:39:44 +01:00

Add error message when passwords don't match in first time flow. Change input field styling in first time flow

This commit is contained in:
Alexander Tseung 2018-05-12 20:53:40 -07:00
parent 6bd1b21d3b
commit 0bcfbc1544
8 changed files with 217 additions and 234 deletions

View File

@ -724,7 +724,7 @@
"message": "New Password (min 8 chars)" "message": "New Password (min 8 chars)"
}, },
"seedPhraseReq": { "seedPhraseReq": {
"message": "seed phrases are 12 words long" "message": "Seed phrases are 12 words long"
}, },
"select": { "select": {
"message": "Select" "message": "Select"

View File

@ -13,8 +13,13 @@ import {
INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE, INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE,
INITIALIZE_NOTICE_ROUTE, INITIALIZE_NOTICE_ROUTE,
} from '../../../../ui/app/routes' } from '../../../../ui/app/routes'
import TextField from '../../../../ui/app/components/text-field'
class CreatePasswordScreen extends Component { class CreatePasswordScreen extends Component {
static contextTypes = {
t: PropTypes.func,
}
static propTypes = { static propTypes = {
isLoading: PropTypes.bool.isRequired, isLoading: PropTypes.bool.isRequired,
createAccount: PropTypes.func.isRequired, createAccount: PropTypes.func.isRequired,
@ -27,6 +32,8 @@ class CreatePasswordScreen extends Component {
state = { state = {
password: '', password: '',
confirmPassword: '', confirmPassword: '',
passwordError: null,
confirmPasswordError: null,
} }
constructor (props) { constructor (props) {
@ -69,82 +76,37 @@ class CreatePasswordScreen extends Component {
.then(() => history.push(INITIALIZE_UNIQUE_IMAGE_ROUTE)) .then(() => history.push(INITIALIZE_UNIQUE_IMAGE_ROUTE))
} }
renderFields () { handlePasswordChange (password) {
const { isMascara, history } = this.props const { confirmPassword } = this.state
let confirmPasswordError = null
let passwordError = null
return ( if (password && password.length < 8) {
<div className={classnames({ 'first-view-main-wrapper': !isMascara })}> passwordError = this.context.t('passwordNotLongEnough')
<div className={classnames({ }
'first-view-main': !isMascara,
'first-view-main__mascara': isMascara, if (confirmPassword && password !== confirmPassword) {
})}> confirmPasswordError = this.context.t('passwordsDontMatch')
{isMascara && <div className="mascara-info first-view-phone-invisible"> }
<Mascot
animationEventEmitter={this.animationEventEmitter} this.setState({ password, passwordError, confirmPasswordError })
width="225" }
height="225"
/> handleConfirmPasswordChange (confirmPassword) {
<div className="info"> const { password } = this.state
MetaMask is a secure identity vault for Ethereum. let confirmPasswordError = null
</div>
<div className="info"> if (password !== confirmPassword) {
It allows you to hold ether & tokens, and interact with decentralized applications. confirmPasswordError = this.context.t('passwordsDontMatch')
</div> }
</div>}
<div className="create-password"> this.setState({ confirmPassword, confirmPasswordError })
<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>
)
} }
render () { render () {
const { history, isMascara } = this.props const { history, isMascara } = this.props
const { passwordError, confirmPasswordError } = this.state
const { t } = this.context
return ( return (
<div className={classnames({ 'first-view-main-wrapper': !isMascara })}> <div className={classnames({ 'first-view-main-wrapper': !isMascara })}>
@ -169,17 +131,30 @@ class CreatePasswordScreen extends Component {
<div className="create-password__title"> <div className="create-password__title">
Create Password Create Password
</div> </div>
<input <TextField
id="create-password"
label={t('newPassword')}
type="password"
className="first-time-flow__input" className="first-time-flow__input"
type="password" value={this.state.password}
placeholder="New Password (min 8 characters)" onChange={event => this.handlePasswordChange(event.target.value)}
onChange={e => this.setState({password: e.target.value})} error={passwordError}
autoFocus
autoComplete="new-password"
margin="normal"
fullWidth
/> />
<input <TextField
className="first-time-flow__input create-password__confirm-input" id="confirm-password"
label={t('confirmPassword')}
type="password" type="password"
placeholder="Confirm Password" className="first-time-flow__input"
onChange={e => this.setState({confirmPassword: e.target.value})} value={this.state.confirmPassword}
onChange={event => this.handleConfirmPasswordChange(event.target.value)}
error={confirmPasswordError}
autoComplete="confirm-password"
margin="normal"
fullWidth
/> />
<button <button
className="first-time-flow__button" className="first-time-flow__button"

View File

@ -1,29 +1,33 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import {connect} from 'react-redux' import {connect} from 'react-redux'
import classnames from 'classnames'
import { import {
createNewVaultAndRestore, createNewVaultAndRestore,
hideWarning,
displayWarning,
unMarkPasswordForgotten, unMarkPasswordForgotten,
} from '../../../../ui/app/actions' } 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 { class ImportSeedPhraseScreen extends Component {
static contextTypes = {
t: PropTypes.func,
}
static propTypes = { static propTypes = {
warning: PropTypes.string, warning: PropTypes.string,
createNewVaultAndRestore: PropTypes.func.isRequired, createNewVaultAndRestore: PropTypes.func.isRequired,
hideWarning: PropTypes.func.isRequired,
displayWarning: PropTypes.func,
leaveImportSeedScreenState: PropTypes.func, leaveImportSeedScreenState: PropTypes.func,
history: PropTypes.object, history: PropTypes.object,
isLoading: PropTypes.bool,
}; };
state = { state = {
seedPhrase: '', seedPhrase: '',
password: '', password: '',
confirmPassword: '', confirmPassword: '',
seedPhraseError: null,
passwordError: null,
confirmPasswordError: null,
} }
parseSeedPhrase = (seedPhrase) => { parseSeedPhrase = (seedPhrase) => {
@ -32,39 +36,47 @@ class ImportSeedPhraseScreen extends Component {
.join(' ') .join(' ')
} }
onChange = ({ seedPhrase, password, confirmPassword }) => { handleSeedPhraseChange (seedPhrase) {
const { let seedPhraseError = null
password: prevPassword,
confirmPassword: prevConfirmPassword,
} = this.state
const { displayWarning, hideWarning } = this.props
let warning = null
if (seedPhrase && this.parseSeedPhrase(seedPhrase).split(' ').length !== 12) { if (seedPhrase && this.parseSeedPhrase(seedPhrase).split(' ').length !== 12) {
warning = 'Seed Phrases are 12 words long' seedPhraseError = this.context.t('seedPhraseReq')
} 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'
} }
if (warning) { this.setState({ seedPhrase, seedPhraseError })
displayWarning(warning) }
} else {
hideWarning() handlePasswordChange (password) {
const { confirmPassword } = this.state
let confirmPasswordError = null
let passwordError = null
if (password && password.length < 8) {
passwordError = this.context.t('passwordNotLongEnough')
} }
seedPhrase && this.setState({ seedPhrase }) if (confirmPassword && password !== confirmPassword) {
password && this.setState({ password }) confirmPasswordError = this.context.t('passwordsDontMatch')
confirmPassword && this.setState({ confirmPassword }) }
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 })
} }
onClick = () => { onClick = () => {
const { password, seedPhrase } = this.state const { password, seedPhrase } = this.state
const { const {
createNewVaultAndRestore, createNewVaultAndRestore,
displayWarning,
leaveImportSeedScreenState, leaveImportSeedScreenState,
history, history,
} = this.props } = this.props
@ -74,10 +86,23 @@ class ImportSeedPhraseScreen extends Component {
.then(() => history.push(INITIALIZE_NOTICE_ROUTE)) .then(() => history.push(INITIALIZE_NOTICE_ROUTE))
} }
hasError () {
const { passwordError, confirmPasswordError, seedPhraseError } = this.state
return passwordError || confirmPasswordError || seedPhraseError
}
render () { render () {
const { seedPhrase, password, confirmPassword } = this.state const {
const { warning, isLoading } = this.props seedPhrase,
const importDisabled = warning || !seedPhrase || !password || !confirmPassword || isLoading password,
confirmPassword,
seedPhraseError,
passwordError,
confirmPasswordError,
} = this.state
const { t } = this.context
const { isLoading } = this.props
const disabled = !seedPhrase || !password || !confirmPassword || isLoading || this.hasError()
return ( return (
<div className="first-view-main-wrapper"> <div className="first-view-main-wrapper">
@ -103,45 +128,40 @@ class ImportSeedPhraseScreen extends Component {
<label className="import-account__input-label">Wallet Seed</label> <label className="import-account__input-label">Wallet Seed</label>
<textarea <textarea
className="import-account__secret-phrase" className="import-account__secret-phrase"
onChange={e => this.onChange({seedPhrase: e.target.value})} onChange={e => this.handleSeedPhraseChange(e.target.value)}
value={this.state.seedPhrase} value={this.state.seedPhrase}
placeholder="Separate each word with a single space" placeholder="Separate each word with a single space"
/> />
</div> </div>
<span <span className="error">
className="error" { seedPhraseError }
>
{this.props.warning}
</span> </span>
<div className="import-account__input-wrapper"> <TextField
<label className="import-account__input-label">New Password</label> id="password"
<input label={t('newPassword')}
className="first-time-flow__input" type="password"
type="password" className="first-time-flow__input"
placeholder="New Password (min 8 characters)" value={this.state.password}
onChange={e => this.onChange({password: e.target.value})} onChange={event => this.handlePasswordChange(event.target.value)}
/> error={passwordError}
</div> autoComplete="new-password"
<div className="import-account__input-wrapper"> margin="normal"
<label />
className={classnames('import-account__input-label', { <TextField
'import-account__input-label__disabled': password.length < 8, id="confirm-password"
})} label={t('confirmPassword')}
>Confirm Password</label> type="password"
<input className="first-time-flow__input"
className={classnames('first-time-flow__input', { value={this.state.confirmPassword}
'first-time-flow__input__disabled': password.length < 8, onChange={event => this.handleConfirmPasswordChange(event.target.value)}
})} error={confirmPasswordError}
type="password" autoComplete="confirm-password"
placeholder="Confirm Password" margin="normal"
onChange={e => this.onChange({confirmPassword: e.target.value})} />
disabled={password.length < 8}
/>
</div>
<button <button
className="first-time-flow__button" className="first-time-flow__button"
onClick={() => !importDisabled && this.onClick()} onClick={() => !disabled && this.onClick()}
disabled={importDisabled} disabled={disabled}
> >
Import Import
</button> </button>
@ -159,7 +179,5 @@ export default connect(
dispatch(unMarkPasswordForgotten()) dispatch(unMarkPasswordForgotten())
}, },
createNewVaultAndRestore: (pw, seed) => dispatch(createNewVaultAndRestore(pw, seed)), createNewVaultAndRestore: (pw, seed) => dispatch(createNewVaultAndRestore(pw, seed)),
displayWarning: (warning) => dispatch(displayWarning(warning)),
hideWarning: () => dispatch(hideWarning()),
}) })
)(ImportSeedPhraseScreen) )(ImportSeedPhraseScreen)

View File

@ -174,10 +174,7 @@
} }
.first-time-flow__input { .first-time-flow__input {
width: initial !important; width: 100%;
font-size: 14px !important;
line-height: 18px !important;
padding: 12px !important;
} }
.tou__body { .tou__body {
@ -248,7 +245,7 @@
} }
.create-password__confirm-input { .create-password__confirm-input {
margin-top: 15px; margin-top: 16px;
} }
.create-password__import-link { .create-password__import-link {
@ -520,10 +517,6 @@ button.backup-phrase__confirm-seed-option:hover {
margin-top: 30px; margin-top: 30px;
} }
.first-time-flow__input--error {
border: 1px solid #FF001F !important;
}
.import-account__input-error-message { .import-account__input-error-message {
margin-top: 10px; margin-top: 10px;
width: 422px; width: 422px;
@ -544,7 +537,13 @@ button.backup-phrase__confirm-seed-option:hover {
} }
.import-account__input { .import-account__input {
width: 325px !important; width: 350px;
}
@media only screen and (max-width: 575px) {
.import-account__input {
width: 100%;
}
} }
.import-account__file-input { .import-account__file-input {
@ -681,20 +680,6 @@ button.backup-phrase__confirm-seed-option:hover {
.first-time-flow__input { .first-time-flow__input {
width: 350px; 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 { .first-time-flow__button {

View File

@ -1,5 +1,4 @@
const PASSWORD = 'password123' const PASSWORD = 'password123'
const reactTriggerChange = require('react-trigger-change')
const { const {
timeout, timeout,
findAsync, findAsync,
@ -11,6 +10,11 @@ async function runFirstTimeUsageTest (assert, done) {
const app = await queryAsync($, '#app-content') 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) await skipNotices(app)
const welcomeButton = (await findAsync(app, '.welcome-screen__button'))[0] 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') assert.equal(title, 'Create Password', 'create password screen')
// enter password // enter password
const pwBox = (await findAsync(app, '.first-time-flow__input'))[0] const pwBox = (await findAsync(app, '#create-password'))[0]
const confBox = (await findAsync(app, '.first-time-flow__input'))[1] const confBox = (await findAsync(app, '#confirm-password'))[0]
pwBox.value = PASSWORD
confBox.value = PASSWORD nativeInputValueSetter.call(pwBox, PASSWORD)
reactTriggerChange(pwBox) pwBox.dispatchEvent(new Event('input', { bubbles: true}))
reactTriggerChange(confBox)
nativeInputValueSetter.call(confBox, PASSWORD)
confBox.dispatchEvent(new Event('input', { bubbles: true}))
// Create Password // Create Password
const createButton = (await findAsync(app, 'button.first-time-flow__button'))[0] const createButton = (await findAsync(app, 'button.first-time-flow__button'))[0]
@ -77,15 +83,8 @@ async function runFirstTimeUsageTest (assert, done) {
pwBox2.focus() pwBox2.focus()
await timeout(1000) await timeout(1000)
// Used to set values on TextField input component
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype, 'value'
).set
nativeInputValueSetter.call(pwBox2, PASSWORD) nativeInputValueSetter.call(pwBox2, PASSWORD)
pwBox2.dispatchEvent(new Event('input', { bubbles: true}))
var ev2 = new Event('input', { bubbles: true})
pwBox2.dispatchEvent(ev2)
const createButton2 = (await findAsync(app, 'button[type="submit"]'))[0] const createButton2 = (await findAsync(app, 'button[type="submit"]'))[0]
createButton2.click() createButton2.click()

View File

@ -74,8 +74,8 @@ async function captureAllScreens() {
await driver.findElement(By.css('button')).click() await driver.findElement(By.css('button')).click()
await captureLanguageScreenShots('create password') await captureLanguageScreenShots('create password')
const passwordBox = await driver.findElement(By.css('input[type=password]:nth-of-type(1)')) const passwordBox = await driver.findElement(By.css('input#create-password'))
const passwordBoxConfirm = await driver.findElement(By.css('input[type=password]:nth-of-type(2)')) const passwordBoxConfirm = await driver.findElement(By.css('input#confirm-password'))
passwordBox.sendKeys('123456789') passwordBox.sendKeys('123456789')
passwordBoxConfirm.sendKeys('123456789') passwordBoxConfirm.sendKeys('123456789')
await delay(500) await delay(500)

View File

@ -8,6 +8,9 @@ const styles = {
'&$cssFocused': { '&$cssFocused': {
color: '#aeaeae', color: '#aeaeae',
}, },
'&$cssError': {
color: '#aeaeae',
},
fontWeight: '400', fontWeight: '400',
color: '#aeaeae', color: '#aeaeae',
}, },
@ -17,6 +20,7 @@ const styles = {
backgroundColor: '#f7861c', backgroundColor: '#f7861c',
}, },
}, },
cssError: {},
} }
const TextField = props => { const TextField = props => {
@ -30,6 +34,7 @@ const TextField = props => {
FormLabelClasses: { FormLabelClasses: {
root: classes.cssLabel, root: classes.cssLabel,
focused: classes.cssFocused, focused: classes.cssFocused,
error: classes.cssError,
}, },
}} }}
InputProps={{ InputProps={{

View File

@ -1,59 +1,60 @@
.welcome-screen { .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; display: flex;
flex-flow: column; flex-flow: column;
justify-content: center;
align-items: center;
font-family: Roboto;
font-weight: 400;
width: 100%; width: 100%;
flex: 1 0 auto; height: 100%;
padding: 70px 0; align-items: center;
background: $white; justify-content: center;
@media screen and (max-width: 575px) { &__header {
padding: 0; font-size: 1.65em;
margin-bottom: 14px;
@media screen and (max-width: 575px) {
font-size: 1.5em;
}
} }
&__info { &__copy {
display: flex; font-size: 1em;
flex-flow: column; width: 400px;
width: 100%; max-width: 90vw;
height: 100%;
align-items: center;
&__header {
font-size: 1.65em;
margin-bottom: 14px;
@media screen and (max-width: 575px) {
font-size: 1.5em;
}
}
&__copy {
font-size: 1em;
width: 400px;
max-width: 90vw;
text-align: center;
@media screen and (max-width: 575px) {
font-size: 0.9em;
}
}
}
&__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;
text-align: center; text-align: center;
text-transform: uppercase;
margin: 35px 0 14px; @media screen and (max-width: 575px) {
transition: 200ms ease-in-out; font-size: .9em;
background-color: rgba(247, 134, 28, 0.9); }
} }
}
&__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);
}
} }