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'
3 import ScriptLoaderMixin from 'react-script-loader'
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 onboardStyles from '../styles/onboarding.scss'
9 import config from '../config'
11 export default React.createClass({
12 displayName: 'PaymentMethodPage',
13 mixins: [ScriptLoaderMixin.ReactScriptLoaderMixin, React.addons.LinkedStateMixin],
15 return 'https://js.stripe.com/v2/'
21 stripeFailedToLoad: false,
32 nameValidationOutputs: {},
33 numberValidationOutputs: {},
34 cvcValidationOutputs: {},
35 expirationValidationOutputs: {},
38 Stripe.setPublishableKey(config.stripeKey)
39 this.setState({stripeLoading: false})
43 this.setState({stripeFailedToLoad: true})
47 let created = new Date()
48 if (app.me && app.me.profile && app.me.profile.created && app.me.profile.created < created) {
49 console.log("using register date...")
50 created = app.me.profile.created
52 let month = created.getMonth()
53 let day = created.getDate()
59 let result = new Date(created.toString())
61 result.setMonth(month)
67 if (this.state.stripeLoading || this.state.stripeFailedToLoad) {
70 this.setState({active: true})
73 Stripe.card.createToken({
74 number: this.state.number,
76 exp_month: this.state.expireMonth,
77 exp_year: this.state.expireYear,
78 name: this.state.name,
79 }, function(status, response) {
83 if (response.error.type == 'card_error') {
84 switch (response.error.code) {
85 case 'incorrect_number':
86 errors.push({'error': 'invalid_value', 'field': '/number'})
88 case 'invalid_number':
89 errors.push({'error': 'invalid_format', 'field': '/number'})
91 case 'invalid_expiry_month':
92 errors.push({'error': 'invalid_format', 'field': '/expireMonth'})
94 case 'invalid_expiry_year':
95 errors.push({'error': 'invalid_format', 'field': '/expireYear'})
98 errors.push({'error': 'invalid_format', 'field': '/cvc'})
101 errors.push({'error': 'invalid_value', 'field': '/expiration'})
103 case 'incorrect_cvc':
104 errors.push({'error': 'invalid_value', 'field': '/cvc'})
106 case 'incorrect_zip':
107 errors.push({'error': 'invalid_value', 'field': '/zip'})
109 case 'card_declined':
110 errors.push({'error': 'insufficient', 'field': '/balance'})
113 errors.push({'error': 'missing', 'field': '/customer/card'})
115 case 'processing_error':
116 errors.push({'error': 'act_of_god', 'field': '/'})
119 errors.push({'error': 'access_denied', 'field': '/rate'})
122 errors.push({'error': 'act_of_god', 'field': '/'})
126 console.log('Error:', response.error.message)
129 console.log('Sending '+response.id+' to server to create customer')
131 t.setState({active: false, errors: errors})
137 <div className='container'>
138 <HeroUnit title='Add a Payment Method'>Gotta keep our servers online and food on the table.</HeroUnit>
139 <article className='onboarding payment'>
140 <p>Ducky costs $2 a month to use. We’ll charge the card you enter below on the first of every month until your account is disabled. You won’t be charged before {this.getChargeDate().toLocaleDateString(navigator.languages, {month: 'long', year: 'numeric', day: 'numeric'})}.</p>
141 <form onSubmit={this.addCard}>
143 <label htmlFor='name'>Cardholder Name</label>
144 <input id='name' type='text' placeholder='This is the name on your card' valueLink={this.linkState('name')} />
145 <ValidationError errors={this.state.errors} field='/name' outputs={this.nameValidationOutputs} />
147 <label htmlFor='cardNumber'>Card Number</label>
148 <input id='cardNumber' type='text' placeholder='4242 4242 4242 4242' valueLink={this.linkState('number')} />
149 <ValidationError errors={this.state.errors} field='/number' outputs={this.numberValidationOutputs} />
151 <label htmlFor='cvc'>Security Code / CVC</label>
152 <input id='cvc' className='cvc' type='password' placeholder='123' valueLink={this.linkState('cvc')} />
154 <label htmlFor='expireMonth' className='expiration'>Expires</label>
155 <input id='expireMonth' className='expiration month' type='text' placeholder='01' valueLink={this.linkState('expireMonth')} />
156 <input id='expireYear' className='expiration year' type='text' placeholder='15' valueLink={this.linkState('expireYear')} />
157 <ValidationError errors={this.state.errors} field='/cvc' outputs={this.cvcValidationOutputs} />
158 <ValidationError errors={this.state.errors} field='/expiration' outputs={this.expirationValidationOutputs} />
160 <div className='actionbuttons'>
161 <button type='button' onClick={this.skip} className='ladda-button' disabled={this.state.active}>Not Now</button>
162 <LaddaButton style='expand-right' active={this.state.active}>
163 <button type='submit' className='primary' disabled={this.state.stripeLoading || this.state.stripeFailedToLoad || this.state.active || this.state.errors.length}>Add Card</button>