ducky/web
ducky/web/src/pages/register.jsx
Persist session data to localStorage. Create a helper library that figures out whether to write to chrome.storage.local or window.localStorage, and unifies their two APIs. Update the Me model to use the getOrFetch method for the profiles collection when retrieving the user's profile. This, unfortunately, makes it an async call (because we may need to fetch data from the server), so we can no longer have it be a derived property, which is a shame. It instead must just be the me.profile() function. Separate out the logic to determine when an access token expires, and turn it into the tokenExpires function. Fill the writeToCache placeholder with the logic to store the current session in either window.localStorage or chrome.storage.local, whichever is the more appropriate, using the helper library. Create the load helper function that will attempt to read session data from localStorage or chrome.storage.local, whichever the library decides is available, and updates the session based on it. Implement the logout function, which just uses the helper library to remove the session data from window.localStorage or chrome.storage.local. We should also be resetting the app.me variable, however. Create a debouncedWriteToCache function that will only write to the cache once every 250 ms, to avoid rushes on the cache. When instantiating our app.me variable, load it in from localStorage or chrome.storage.local if we can. Also, listen for changes to app.me, and persist them to chrome.storage.local or localStorage.
| paddy@0 | 1 import app from 'ampersand-app' |
| paddy@0 | 2 import React from 'react/addons' |
| paddy@0 | 3 import localLinks from 'local-links' |
| paddy@0 | 4 import LaddaButton from 'react-ladda' |
| paddy@0 | 5 import LaddaCSS from '../../node_modules/ladda/dist/ladda.min.css' |
| paddy@0 | 6 import HeroUnit from '../components/hero' |
| paddy@2 | 7 import ValidationError from '../components/validation-error' |
| paddy@0 | 8 import onboardingStyles from '../styles/onboarding.scss' |
| paddy@2 | 9 import flashStyles from '../styles/_flashes.scss' |
| paddy@2 | 10 import debounce from 'lodash.debounce' |
| paddy@0 | 11 |
| paddy@0 | 12 export default React.createClass({ |
| paddy@0 | 13 displayName: 'RegisterPage', |
| paddy@0 | 14 mixins: [React.addons.LinkedStateMixin], |
| paddy@0 | 15 |
| paddy@0 | 16 getInitialState () { |
| paddy@0 | 17 return { |
| paddy@0 | 18 email: null, |
| paddy@0 | 19 emailConfirmation: null, |
| paddy@0 | 20 passphrase: null, |
| paddy@0 | 21 passphraseConfirmation: null, |
| paddy@0 | 22 active: false, |
| paddy@4 | 23 clientErrors: [], |
| paddy@4 | 24 serverErrors: [], |
| paddy@0 | 25 } |
| paddy@0 | 26 }, |
| paddy@0 | 27 |
| paddy@2 | 28 emailValidationOutputs: { |
| paddy@2 | 29 'missing': 'We need to know how to contact you. We promise not to share it.', |
| paddy@2 | 30 'overflow': 'Hm, that’s a bit long. Do you have a shorter email address?', |
| paddy@2 | 31 'invalid_format': 'That doesn’t look like an email address… Double check it?', |
| paddy@7 | 32 'conflict': 'Hm, that email already has an account. Did you forget your password?', |
| paddy@2 | 33 }, |
| paddy@2 | 34 |
| paddy@2 | 35 emailConfirmationValidationOutputs: { |
| paddy@2 | 36 'invalid_value': 'Oops! Those emails don’t match. Maybe double-check them?', |
| paddy@2 | 37 }, |
| paddy@2 | 38 |
| paddy@2 | 39 passphraseValidationOutputs: { |
| paddy@2 | 40 'insufficient': 'A longer passphrase would be better.', |
| paddy@2 | 41 'missing': 'Looks like you forgot to enter a passphrase. You need one!', |
| paddy@2 | 42 'overflow': 'We can’t store that long a passphrase. Try a shorter one.', |
| paddy@2 | 43 }, |
| paddy@2 | 44 |
| paddy@2 | 45 passphraseConfirmationValidationOutputs: { |
| paddy@2 | 46 'invalid_value': 'Oops! Those passphrases don’t match. Maybe double-check them?', |
| paddy@2 | 47 }, |
| paddy@2 | 48 |
| paddy@6 | 49 catchAllValidationOutputs: { |
| paddy@7 | 50 'act_of_god': 'Hm, something went wrong. Try again? Or let support know.', |
| paddy@7 | 51 'invalid_form': 'Uh oh, things went really wrong. Let support know you saw the dreaded invalid_format error!', |
| paddy@7 | 52 'conflict': 'Something went wrong, but trying again will probably fix it.', |
| paddy@7 | 53 'access_denied': 'Oops, something’s gone awry. Let support know your client isn’t working.', |
| paddy@6 | 54 }, |
| paddy@6 | 55 |
| paddy@2 | 56 debouncedValidateForm (event) { |
| paddy@2 | 57 this._validateForm(event) |
| paddy@2 | 58 }, |
| paddy@2 | 59 |
| paddy@2 | 60 _validateForm (event) { |
| paddy@2 | 61 const fields = { |
| paddy@2 | 62 email: this.state.email, |
| paddy@2 | 63 emailConfirmation: this.state.emailConfirmation, |
| paddy@2 | 64 passphrase: this.state.passphrase, |
| paddy@2 | 65 passphraseConfirmation: this.state.passphraseConfirmation, |
| paddy@2 | 66 } |
| paddy@2 | 67 if (event != null) { |
| paddy@2 | 68 if (event.target.id == 'emailInput') { |
| paddy@2 | 69 fields.email = event.target.value |
| paddy@2 | 70 } |
| paddy@2 | 71 else if (event.target.id == 'emailConfirmationInput') { |
| paddy@2 | 72 fields.emailConfirmation = event.target.value |
| paddy@2 | 73 } |
| paddy@2 | 74 else if (event.target.id == 'passphraseInput') { |
| paddy@2 | 75 fields.passphrase = event.target.value |
| paddy@2 | 76 } |
| paddy@2 | 77 else if (event.target.id == 'passphraseConfirmationInput') { |
| paddy@2 | 78 fields.passphraseConfirmation = event.target.value |
| paddy@2 | 79 } |
| paddy@2 | 80 } |
| paddy@2 | 81 const errors = this.validate(fields, event == null) |
| paddy@7 | 82 this.setState({clientErrors: errors, serverErrors: []}) |
| paddy@2 | 83 return errors.length <= 0 |
| paddy@2 | 84 }, |
| paddy@2 | 85 |
| paddy@2 | 86 validateForm (event) { |
| paddy@2 | 87 event.persist() |
| paddy@2 | 88 this.debouncedValidateForm(event) |
| paddy@2 | 89 }, |
| paddy@2 | 90 |
| paddy@2 | 91 validate (fields, all) { |
| paddy@2 | 92 const errors = [] |
| paddy@2 | 93 if (fields.email != null) { |
| paddy@2 | 94 if (fields.email.length == 0) { |
| paddy@2 | 95 errors.push({'error': 'missing', 'field': '/email'}) |
| paddy@2 | 96 } else if (fields.email.length > 64) { |
| paddy@2 | 97 errors.push({'error': 'overflow', 'field': '/email'}) |
| paddy@2 | 98 } else if (!fields.email.match(/.+@.+\..+/)) { |
| paddy@2 | 99 errors.push({'error': 'invalid_format', 'field': '/email'}) |
| paddy@2 | 100 } |
| paddy@2 | 101 } else if (all) { |
| paddy@2 | 102 errors.push({'error': 'missing', 'field': '/email'}) |
| paddy@2 | 103 } |
| paddy@2 | 104 if (fields.emailConfirmation != null || all) { |
| paddy@2 | 105 if (fields.emailConfirmation != fields.email && (fields.email || fields.emailConfirmation)) { |
| paddy@2 | 106 errors.push({'error': 'invalid_value', 'field': '/email_confirmation'}) |
| paddy@2 | 107 } |
| paddy@2 | 108 } |
| paddy@2 | 109 if (fields.passphrase != null) { |
| paddy@2 | 110 if (fields.passphrase.length == 0) { |
| paddy@2 | 111 errors.push({'error': 'missing', 'field': '/passphrase'}) |
| paddy@2 | 112 } else if (fields.passphrase.length < 6) { |
| paddy@2 | 113 errors.push({'error': 'insufficient', 'field': '/passphrase'}) |
| paddy@2 | 114 } else if (fields.passphrase.length > 64) { |
| paddy@2 | 115 errors.push({'error': 'overflow', 'field': '/passphrase'}) |
| paddy@2 | 116 } |
| paddy@2 | 117 } else if (all) { |
| paddy@2 | 118 errors.push({'error': 'missing', 'field': '/passphrase'}) |
| paddy@2 | 119 } |
| paddy@2 | 120 if (fields.passphraseConfirmation != null || all) { |
| paddy@2 | 121 if (fields.passphraseConfirmation != fields.passphrase && (fields.passphrase || fields.passphraseConfirmation)) { |
| paddy@2 | 122 errors.push({'error': 'invalid_value', 'field': '/passphrase_confirmation'}) |
| paddy@2 | 123 } |
| paddy@2 | 124 } |
| paddy@2 | 125 return errors |
| paddy@2 | 126 }, |
| paddy@2 | 127 |
| paddy@0 | 128 componentDidMount () { |
| paddy@0 | 129 app.profiles.on('request', (moc, xhr, options) => { |
| paddy@0 | 130 this.setState({active: true}) |
| paddy@0 | 131 }) |
| paddy@0 | 132 app.profiles.on('error', (moc, xhr, options) => { |
| paddy@4 | 133 let state = {active: false} |
| paddy@7 | 134 let resp = {} |
| paddy@7 | 135 if (xhr && xhr.response) { |
| paddy@7 | 136 resp = JSON.parse(xhr.response) |
| paddy@7 | 137 } |
| paddy@7 | 138 if (resp.errors && resp.errors.length) { |
| paddy@7 | 139 state.serverErrors = resp.errors |
| paddy@4 | 140 } else { |
| paddy@4 | 141 state.serverErrors = [{'error': 'act_of_god'}] |
| paddy@4 | 142 } |
| paddy@4 | 143 this.setState(state) |
| paddy@0 | 144 }) |
| paddy@0 | 145 app.profiles.on('sync', (moc, xhr, options) => { |
| paddy@0 | 146 app.me.login(this.state.email, this.state.passphrase) |
| paddy@0 | 147 }) |
| paddy@0 | 148 app.me.on('sync', (moc, xhr, options) => { |
| paddy@0 | 149 this.setState({active: false}) |
| paddy@2 | 150 app.router.navigate('/register/payment') |
| paddy@0 | 151 }) |
| paddy@4 | 152 app.me.on('error', (moc, xhr, options) => { |
| paddy@4 | 153 let state = {active: false} |
| paddy@7 | 154 let resp = {} |
| paddy@7 | 155 if (xhr && xhr.response) { |
| paddy@7 | 156 resp = JSON.parse(xhr.response) |
| paddy@7 | 157 } |
| paddy@7 | 158 console.log(resp) |
| paddy@7 | 159 if (resp.errors && resp.errors.length) { |
| paddy@7 | 160 state.serverErrors = resp.errors |
| paddy@7 | 161 } else if (resp.error && resp.error == 'invalid_client') { |
| paddy@7 | 162 state.serverErrors = [{'error': 'access_denied'}] |
| paddy@4 | 163 } else { |
| paddy@4 | 164 state.serverErrors = [{'error': 'act_of_god'}] |
| paddy@4 | 165 } |
| paddy@4 | 166 this.setState(state) |
| paddy@4 | 167 }) |
| paddy@0 | 168 }, |
| paddy@0 | 169 |
| paddy@2 | 170 componentWillMount () { |
| paddy@2 | 171 this.debouncedValidateForm = debounce(this.debouncedValidateForm, 900) |
| paddy@2 | 172 }, |
| paddy@2 | 173 |
| paddy@0 | 174 register (e) { |
| paddy@0 | 175 e.preventDefault() |
| paddy@2 | 176 const success = this._validateForm(null) |
| paddy@2 | 177 if (!success) { |
| paddy@2 | 178 return |
| paddy@2 | 179 } |
| paddy@0 | 180 app.profiles.register(this.state.email, this.state.passphrase) |
| paddy@0 | 181 }, |
| paddy@0 | 182 |
| paddy@0 | 183 onBackClick (event) { |
| paddy@0 | 184 event.preventDefault() |
| paddy@0 | 185 window.history.back() |
| paddy@0 | 186 }, |
| paddy@0 | 187 |
| paddy@0 | 188 render () { |
| paddy@0 | 189 return ( |
| paddy@0 | 190 <div className='container'> |
| paddy@0 | 191 <HeroUnit title='Create an Account'>We’d like to get to know you better.</HeroUnit> |
| paddy@0 | 192 <article className='onboarding register'> |
| paddy@0 | 193 <form onSubmit={this.register}> |
| paddy@0 | 194 <div> |
| paddy@2 | 195 <label htmlFor='emailInput'>Email</label> |
| paddy@2 | 196 <input id='emailInput' type='email' placeholder='Ours is quack@useducky.com' valueLink={this.linkState('email')} disabled={this.state.active} onInput={this.validateForm} /> |
| paddy@4 | 197 <ValidationError errors={this.state.clientErrors.concat(this.state.serverErrors)} field='/email' outputs={this.emailValidationOutputs} /> |
| paddy@0 | 198 |
| paddy@2 | 199 <label htmlFor='emailConfirmationInput'>Verify Email</label> |
| paddy@2 | 200 <input id='emailConfirmationInput' type='email' placeholder='Typos are the absolute worst.' valueLink={this.linkState('emailConfirmation')} disabled={this.state.active} onInput={this.validateForm} /> |
| paddy@4 | 201 <ValidationError errors={this.state.clientErrors.concat(this.state.serverErrors)} field='/email_confirmation' outputs={this.emailConfirmationValidationOutputs} /> |
| paddy@0 | 202 |
| paddy@2 | 203 <label htmlFor='passphraseInput'>Passphrase</label> |
| paddy@2 | 204 <input id='passphraseInput' type='password' placeholder='We use a sentence. Try it!' valueLink={this.linkState('passphrase')} disabled={this.state.active} onInput={this.validateForm} /> |
| paddy@4 | 205 <ValidationError errors={this.state.clientErrors.concat(this.state.serverErrors)} field='/passphrase' outputs={this.passphraseValidationOutputs} /> |
| paddy@0 | 206 |
| paddy@2 | 207 <label htmlFor='passphraseConfirmationInput'>Verify Passphrase</label> |
| paddy@2 | 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} /> |
| paddy@4 | 209 <ValidationError errors={this.state.clientErrors.concat(this.state.serverErrors)} field='/passphrase_confirmation' outputs={this.passphraseConfirmationValidationOutputs} /> |
| paddy@6 | 210 |
| paddy@6 | 211 <ValidationError errors={this.state.clientErrors.concat(this.state.serverErrors)} notFields={['/email', '/email_confirmation', '/passphrase', '/passphrase_confirmation']} notHeaders={[]} notParams={[]} outputs={this.catchAllValidationOutputs} /> |
| paddy@0 | 212 </div> |
| paddy@0 | 213 <div className='actionbuttons'> |
| paddy@0 | 214 <button onClick={this.onBackClick} disabled={this.state.active} type='button' className='ladda-button'>Back</button> |
| paddy@0 | 215 <LaddaButton style='expand-right' active={this.state.active}> |
| paddy@4 | 216 <button type='submit' className='primary' disabled={this.state.active || this.state.clientErrors.length}>Register</button> |
| paddy@0 | 217 </LaddaButton> |
| paddy@0 | 218 </div> |
| paddy@0 | 219 </form> |
| paddy@0 | 220 </article> |
| paddy@0 | 221 </div> |
| paddy@0 | 222 ) |
| paddy@0 | 223 } |
| paddy@0 | 224 }) |