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.
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,
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?',
33 emailConfirmationValidationOutputs: {
34 'invalid_value': 'Oops! Those emails don’t match. Maybe double-check them?',
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.',
43 passphraseConfirmationValidationOutputs: {
44 'invalid_value': 'Oops! Those passphrases don’t match. Maybe double-check them?',
47 debouncedValidateForm (event) {
48 this._validateForm(event)
51 _validateForm (event) {
53 email: this.state.email,
54 emailConfirmation: this.state.emailConfirmation,
55 passphrase: this.state.passphrase,
56 passphraseConfirmation: this.state.passphraseConfirmation,
59 if (event.target.id == 'emailInput') {
60 fields.email = event.target.value
62 else if (event.target.id == 'emailConfirmationInput') {
63 fields.emailConfirmation = event.target.value
65 else if (event.target.id == 'passphraseInput') {
66 fields.passphrase = event.target.value
68 else if (event.target.id == 'passphraseConfirmationInput') {
69 fields.passphraseConfirmation = event.target.value
72 const errors = this.validate(fields, event == null)
73 this.setState({errors: errors})
74 return errors.length <= 0
77 validateForm (event) {
79 this.debouncedValidateForm(event)
82 validate (fields, all) {
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'})
93 errors.push({'error': 'missing', 'field': '/email'})
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'})
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'})
109 errors.push({'error': 'missing', 'field': '/passphrase'})
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'})
119 componentDidMount () {
120 app.profiles.on('request', (moc, xhr, options) => {
121 this.setState({active: true})
123 app.profiles.on('error', (moc, xhr, options) => {
124 this.setState({active: false})
126 app.profiles.on('sync', (moc, xhr, options) => {
127 app.me.login(this.state.email, this.state.passphrase)
129 app.me.on('sync', (moc, xhr, options) => {
130 this.setState({active: false})
131 app.router.navigate('/register/payment')
135 componentWillMount () {
136 this.debouncedValidateForm = debounce(this.debouncedValidateForm, 900)
141 const success = this._validateForm(null)
145 app.profiles.register(this.state.email, this.state.passphrase)
148 onBackClick (event) {
149 event.preventDefault()
150 window.history.back()
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}>
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} />
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>