Add error handling messages, clear server errors on validation.
Add missing messages for error conditions that our server can return.
Clear our server error messages when client-side validation occurs, so you
aren't confusingly left with errors after you change the input that caused them.
Ideally, this behaviour would be limited to just the errors that were caused by
the updated field, but clearing all of them seems to be the more user-friendly
behaviour than just leaving them. Baby steps.
Fix a bug in our handling of error messages--ampersand-sync is kind of
inconsistent, and parses our successful responses as JSON, but neglects to parse
the error responses as JSON. So we need to manually parse our errors from JSON
before we can work with them.
We also added a handler for invalid_client error messages, which don't match our
error objects (thanks OAuth2 spec!), so we translate it to one of our error
objects and then add it to the server errors to be displayed.
1 import app from 'ampersand-app'
2 import React from 'react/addons'
3 import localLinks from 'local-links'
4 import LaddaButton from 'react-ladda'
5 import LaddaCSS from '../../node_modules/ladda/dist/ladda.min.css'
6 import HeroUnit from '../components/hero'
7 import ValidationError from '../components/validation-error'
8 import onboardingStyles from '../styles/onboarding.scss'
9 import flashStyles from '../styles/_flashes.scss'
10 import debounce from 'lodash.debounce'
12 export default React.createClass({
13 displayName: 'RegisterPage',
14 mixins: [React.addons.LinkedStateMixin],
19 emailConfirmation: null,
21 passphraseConfirmation: null,
28 emailValidationOutputs: {
29 'missing': 'We need to know how to contact you. We promise not to share it.',
30 'overflow': 'Hm, that’s a bit long. Do you have a shorter email address?',
31 'invalid_format': 'That doesn’t look like an email address… Double check it?',
32 'conflict': 'Hm, that email already has an account. Did you forget your password?',
35 emailConfirmationValidationOutputs: {
36 'invalid_value': 'Oops! Those emails don’t match. Maybe double-check them?',
39 passphraseValidationOutputs: {
40 'insufficient': 'A longer passphrase would be better.',
41 'missing': 'Looks like you forgot to enter a passphrase. You need one!',
42 'overflow': 'We can’t store that long a passphrase. Try a shorter one.',
45 passphraseConfirmationValidationOutputs: {
46 'invalid_value': 'Oops! Those passphrases don’t match. Maybe double-check them?',
49 catchAllValidationOutputs: {
50 'act_of_god': 'Hm, something went wrong. Try again? Or let support know.',
51 'invalid_form': 'Uh oh, things went really wrong. Let support know you saw the dreaded invalid_format error!',
52 'conflict': 'Something went wrong, but trying again will probably fix it.',
53 'access_denied': 'Oops, something’s gone awry. Let support know your client isn’t working.',
56 debouncedValidateForm (event) {
57 this._validateForm(event)
60 _validateForm (event) {
62 email: this.state.email,
63 emailConfirmation: this.state.emailConfirmation,
64 passphrase: this.state.passphrase,
65 passphraseConfirmation: this.state.passphraseConfirmation,
68 if (event.target.id == 'emailInput') {
69 fields.email = event.target.value
71 else if (event.target.id == 'emailConfirmationInput') {
72 fields.emailConfirmation = event.target.value
74 else if (event.target.id == 'passphraseInput') {
75 fields.passphrase = event.target.value
77 else if (event.target.id == 'passphraseConfirmationInput') {
78 fields.passphraseConfirmation = event.target.value
81 const errors = this.validate(fields, event == null)
82 this.setState({clientErrors: errors, serverErrors: []})
83 return errors.length <= 0
86 validateForm (event) {
88 this.debouncedValidateForm(event)
91 validate (fields, all) {
93 if (fields.email != null) {
94 if (fields.email.length == 0) {
95 errors.push({'error': 'missing', 'field': '/email'})
96 } else if (fields.email.length > 64) {
97 errors.push({'error': 'overflow', 'field': '/email'})
98 } else if (!fields.email.match(/.+@.+\..+/)) {
99 errors.push({'error': 'invalid_format', 'field': '/email'})
102 errors.push({'error': 'missing', 'field': '/email'})
104 if (fields.emailConfirmation != null || all) {
105 if (fields.emailConfirmation != fields.email && (fields.email || fields.emailConfirmation)) {
106 errors.push({'error': 'invalid_value', 'field': '/email_confirmation'})
109 if (fields.passphrase != null) {
110 if (fields.passphrase.length == 0) {
111 errors.push({'error': 'missing', 'field': '/passphrase'})
112 } else if (fields.passphrase.length < 6) {
113 errors.push({'error': 'insufficient', 'field': '/passphrase'})
114 } else if (fields.passphrase.length > 64) {
115 errors.push({'error': 'overflow', 'field': '/passphrase'})
118 errors.push({'error': 'missing', 'field': '/passphrase'})
120 if (fields.passphraseConfirmation != null || all) {
121 if (fields.passphraseConfirmation != fields.passphrase && (fields.passphrase || fields.passphraseConfirmation)) {
122 errors.push({'error': 'invalid_value', 'field': '/passphrase_confirmation'})
128 componentDidMount () {
129 app.profiles.on('request', (moc, xhr, options) => {
130 this.setState({active: true})
132 app.profiles.on('error', (moc, xhr, options) => {
133 let state = {active: false}
135 if (xhr && xhr.response) {
136 resp = JSON.parse(xhr.response)
138 if (resp.errors && resp.errors.length) {
139 state.serverErrors = resp.errors
141 state.serverErrors = [{'error': 'act_of_god'}]
145 app.profiles.on('sync', (moc, xhr, options) => {
146 app.me.login(this.state.email, this.state.passphrase)
148 app.me.on('sync', (moc, xhr, options) => {
149 this.setState({active: false})
150 app.router.navigate('/register/payment')
152 app.me.on('error', (moc, xhr, options) => {
153 let state = {active: false}
155 if (xhr && xhr.response) {
156 resp = JSON.parse(xhr.response)
159 if (resp.errors && resp.errors.length) {
160 state.serverErrors = resp.errors
161 } else if (resp.error && resp.error == 'invalid_client') {
162 state.serverErrors = [{'error': 'access_denied'}]
164 state.serverErrors = [{'error': 'act_of_god'}]
170 componentWillMount () {
171 this.debouncedValidateForm = debounce(this.debouncedValidateForm, 900)
176 const success = this._validateForm(null)
180 app.profiles.register(this.state.email, this.state.passphrase)
183 onBackClick (event) {
184 event.preventDefault()
185 window.history.back()
190 <div className='container'>
191 <HeroUnit title='Create an Account'>We’d like to get to know you better.</HeroUnit>
192 <article className='onboarding register'>
193 <form onSubmit={this.register}>
195 <label htmlFor='emailInput'>Email</label>
196 <input id='emailInput' type='email' placeholder='Ours is quack@useducky.com' valueLink={this.linkState('email')} disabled={this.state.active} onInput={this.validateForm} />
197 <ValidationError errors={this.state.clientErrors.concat(this.state.serverErrors)} field='/email' outputs={this.emailValidationOutputs} />
199 <label htmlFor='emailConfirmationInput'>Verify Email</label>
200 <input id='emailConfirmationInput' type='email' placeholder='Typos are the absolute worst.' valueLink={this.linkState('emailConfirmation')} disabled={this.state.active} onInput={this.validateForm} />
201 <ValidationError errors={this.state.clientErrors.concat(this.state.serverErrors)} field='/email_confirmation' outputs={this.emailConfirmationValidationOutputs} />
203 <label htmlFor='passphraseInput'>Passphrase</label>
204 <input id='passphraseInput' type='password' placeholder='We use a sentence. Try it!' valueLink={this.linkState('passphrase')} disabled={this.state.active} onInput={this.validateForm} />
205 <ValidationError errors={this.state.clientErrors.concat(this.state.serverErrors)} field='/passphrase' outputs={this.passphraseValidationOutputs} />
207 <label htmlFor='passphraseConfirmationInput'>Verify Passphrase</label>
208 <input id='passphraseConfirmationInput' type='password' placeholder='Just to make sure you know it.' valueLink={this.linkState('passphraseConfirmation')} disabled={this.state.active} onInput={this.validateForm} />
209 <ValidationError errors={this.state.clientErrors.concat(this.state.serverErrors)} field='/passphrase_confirmation' outputs={this.passphraseConfirmationValidationOutputs} />
211 <ValidationError errors={this.state.clientErrors.concat(this.state.serverErrors)} notFields={['/email', '/email_confirmation', '/passphrase', '/passphrase_confirmation']} notHeaders={[]} notParams={[]} outputs={this.catchAllValidationOutputs} />
213 <div className='actionbuttons'>
214 <button onClick={this.onBackClick} disabled={this.state.active} type='button' className='ladda-button'>Back</button>
215 <LaddaButton style='expand-right' active={this.state.active}>
216 <button type='submit' className='primary' disabled={this.state.active || this.state.clientErrors.length}>Register</button>