ducky/web

Paddy 2015-05-29 Parent:99a43a6d1d30 Child:bd64a7d043d0

2:b9d0efb44eaa Go to Latest

ducky/web/src/pages/register.jsx

Validate registration, add payment page. Add lodash.debounce to debounce our validation. Update react-ladda to take advantage of the bugfix that will make it disable the button properly. Use react-script-loader to load stripe.js for our payment page async. No, I'm not terrified that I'm planning on shipping the mission-critical version of my software (the part where I get _paid_) using v0.0.1 software. Should I be? Add jwt-decode so we can decode the access tokens we get in response. We can't _verify_ them, but since we're not a source of truth, that really doesn't make much a difference. Everything is considered to be tamperable, anyways, so we never rely on the client being authoritative or safe. Add a `npm run deploy` task that will deploy the damn project to surge. Create a ValidationError component that will look through a set of errors for the ones that apply to it, and display the appropriate message for them. Update our config.js with a new (bullshit) clientID and clientSecret. It would help if we used an actual database to store these instead of storing them in memory, so I didn't have to generate them fresh every time. But then I'd have to delete accounts, too. Where's the fun in that? We also added a stripeKey which is the publishable key for our test account. Suck it, scraper bots. Use jwt-decode to decode the access token we receive into a claim we can pull the ProfileID out of. The Me model gains a profileID attribute to store this new information, so we can tell who the eff is signed in. We also used the ever wonderful derived properties feature to make a me.profile, which will always resolve to the profile of the signed in user, as it exists (stored locally). We should probably do some fallback checking to make it fetch the profile from the server, huh? I'll open an issue. Create our payment page. this is scary similar to our registration page, except we're loading stripe.js async (and disabling the buttons until it's loaded), and we're asking for the credit card details, instead of their email and password. We then trade the credit card details for a token (hooray, no need to deal with PCI compliance!) and we'll eventually send that token to our server, to create a customer. Taking money is complicated. Hilariously, we have the code to turn all the Stripe server errors into errors our ValidationError component can handle, and we have the ValidationError components placed, but we haven't defined the error messages for them yet. Oops. We also haven't done any kind of local validation. Oopsie. We used our ValidationError to set up errors for our register page, and also used the debounced validator to check the information while we type. What's not to love? Haven't quite gotten around to handling server errors yet. It's on the to-do list. Also, how hilarious is it that we need three freaking methods to debounce our validation properly? Once we register a profile, we should continue on to the payment method part of the program, so we tossed that router.navigate call in there. Finally, we updated our IDs, because reasons and typing. We also included a new _flashes.scss page that will take care of our error display, and added payment page styles to our onboarding.scss file. Finally, we updated our webpack config to output all the files to build/static so we can use build/ as our deploy folder, with an index.html in the root of it.

History
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],
16 getInitialState () {
17 return {
18 email: null,
19 emailConfirmation: null,
20 passphrase: null,
21 passphraseConfirmation: null,
22 active: false,
23 errors: [],
24 }
25 },
27 emailValidationOutputs: {
28 'missing': 'We need to know how to contact you. We promise not to share it.',
29 'overflow': 'Hm, that’s a bit long. Do you have a shorter email address?',
30 'invalid_format': 'That doesn’t look like an email address… Double check it?',
31 },
33 emailConfirmationValidationOutputs: {
34 'invalid_value': 'Oops! Those emails don’t match. Maybe double-check them?',
35 },
37 passphraseValidationOutputs: {
38 'insufficient': 'A longer passphrase would be better.',
39 'missing': 'Looks like you forgot to enter a passphrase. You need one!',
40 'overflow': 'We can’t store that long a passphrase. Try a shorter one.',
41 },
43 passphraseConfirmationValidationOutputs: {
44 'invalid_value': 'Oops! Those passphrases don’t match. Maybe double-check them?',
45 },
47 debouncedValidateForm (event) {
48 this._validateForm(event)
49 },
51 _validateForm (event) {
52 const fields = {
53 email: this.state.email,
54 emailConfirmation: this.state.emailConfirmation,
55 passphrase: this.state.passphrase,
56 passphraseConfirmation: this.state.passphraseConfirmation,
57 }
58 if (event != null) {
59 if (event.target.id == 'emailInput') {
60 fields.email = event.target.value
61 }
62 else if (event.target.id == 'emailConfirmationInput') {
63 fields.emailConfirmation = event.target.value
64 }
65 else if (event.target.id == 'passphraseInput') {
66 fields.passphrase = event.target.value
67 }
68 else if (event.target.id == 'passphraseConfirmationInput') {
69 fields.passphraseConfirmation = event.target.value
70 }
71 }
72 const errors = this.validate(fields, event == null)
73 this.setState({errors: errors})
74 return errors.length <= 0
75 },
77 validateForm (event) {
78 event.persist()
79 this.debouncedValidateForm(event)
80 },
82 validate (fields, all) {
83 const errors = []
84 if (fields.email != null) {
85 if (fields.email.length == 0) {
86 errors.push({'error': 'missing', 'field': '/email'})
87 } else if (fields.email.length > 64) {
88 errors.push({'error': 'overflow', 'field': '/email'})
89 } else if (!fields.email.match(/.+@.+\..+/)) {
90 errors.push({'error': 'invalid_format', 'field': '/email'})
91 }
92 } else if (all) {
93 errors.push({'error': 'missing', 'field': '/email'})
94 }
95 if (fields.emailConfirmation != null || all) {
96 if (fields.emailConfirmation != fields.email && (fields.email || fields.emailConfirmation)) {
97 errors.push({'error': 'invalid_value', 'field': '/email_confirmation'})
98 }
99 }
100 if (fields.passphrase != null) {
101 if (fields.passphrase.length == 0) {
102 errors.push({'error': 'missing', 'field': '/passphrase'})
103 } else if (fields.passphrase.length < 6) {
104 errors.push({'error': 'insufficient', 'field': '/passphrase'})
105 } else if (fields.passphrase.length > 64) {
106 errors.push({'error': 'overflow', 'field': '/passphrase'})
107 }
108 } else if (all) {
109 errors.push({'error': 'missing', 'field': '/passphrase'})
110 }
111 if (fields.passphraseConfirmation != null || all) {
112 if (fields.passphraseConfirmation != fields.passphrase && (fields.passphrase || fields.passphraseConfirmation)) {
113 errors.push({'error': 'invalid_value', 'field': '/passphrase_confirmation'})
114 }
115 }
116 return errors
117 },
119 componentDidMount () {
120 app.profiles.on('request', (moc, xhr, options) => {
121 this.setState({active: true})
122 })
123 app.profiles.on('error', (moc, xhr, options) => {
124 this.setState({active: false})
125 })
126 app.profiles.on('sync', (moc, xhr, options) => {
127 app.me.login(this.state.email, this.state.passphrase)
128 })
129 app.me.on('sync', (moc, xhr, options) => {
130 this.setState({active: false})
131 app.router.navigate('/register/payment')
132 })
133 },
135 componentWillMount () {
136 this.debouncedValidateForm = debounce(this.debouncedValidateForm, 900)
137 },
139 register (e) {
140 e.preventDefault()
141 const success = this._validateForm(null)
142 if (!success) {
143 return
144 }
145 app.profiles.register(this.state.email, this.state.passphrase)
146 },
148 onBackClick (event) {
149 event.preventDefault()
150 window.history.back()
151 },
153 render () {
154 return (
155 <div className='container'>
156 <HeroUnit title='Create an Account'>We’d like to get to know you better.</HeroUnit>
157 <article className='onboarding register'>
158 <form onSubmit={this.register}>
159 <div>
160 <label htmlFor='emailInput'>Email</label>
161 <input id='emailInput' type='email' placeholder='Ours is quack@useducky.com' valueLink={this.linkState('email')} disabled={this.state.active} onInput={this.validateForm} />
162 <ValidationError errors={this.state.errors} field='/email' outputs={this.emailValidationOutputs} />
164 <label htmlFor='emailConfirmationInput'>Verify Email</label>
165 <input id='emailConfirmationInput' type='email' placeholder='Typos are the absolute worst.' valueLink={this.linkState('emailConfirmation')} disabled={this.state.active} onInput={this.validateForm} />
166 <ValidationError errors={this.state.errors} field='/email_confirmation' outputs={this.emailConfirmationValidationOutputs} />
168 <label htmlFor='passphraseInput'>Passphrase</label>
169 <input id='passphraseInput' type='password' placeholder='We use a sentence. Try it!' valueLink={this.linkState('passphrase')} disabled={this.state.active} onInput={this.validateForm} />
170 <ValidationError errors={this.state.errors} field='/passphrase' outputs={this.passphraseValidationOutputs} />
172 <label htmlFor='passphraseConfirmationInput'>Verify Passphrase</label>
173 <input id='passphraseConfirmationInput' type='password' placeholder='Just to make sure you know it.' valueLink={this.linkState('passphraseConfirmation')} disabled={this.state.active} onInput={this.validateForm} />
174 <ValidationError errors={this.state.errors} field='/passphrase_confirmation' outputs={this.passphraseConfirmationValidationOutputs} />
175 </div>
176 <div className='actionbuttons'>
177 <button onClick={this.onBackClick} disabled={this.state.active} type='button' className='ladda-button'>Back</button>
178 <LaddaButton style='expand-right' active={this.state.active}>
179 <button type='submit' className='primary' disabled={this.state.active || this.state.errors.length}>Register</button>
180 </LaddaButton>
181 </div>
182 </form>
183 </article>
184 </div>
185 )
186 }
187 })