ducky/web

Paddy 2015-06-04 Parent:b9d0efb44eaa Child:e9e0a28a7419

8:62e0c0df28bb Go to Latest

ducky/web/src/pages/payment.jsx

Display server error messages when adding card. When adding a credit card, correctly display user-friendly messages.

History
paddy@2 1 import app from 'ampersand-app'
paddy@2 2 import React from 'react'
paddy@2 3 import ScriptLoaderMixin from 'react-script-loader'
paddy@2 4 import LaddaButton from 'react-ladda'
paddy@2 5 import LaddaCSS from '../../node_modules/ladda/dist/ladda.min.css'
paddy@2 6 import HeroUnit from '../components/hero'
paddy@2 7 import ValidationError from '../components/validation-error'
paddy@2 8 import onboardStyles from '../styles/onboarding.scss'
paddy@2 9 import config from '../config'
paddy@2 10
paddy@2 11 export default React.createClass({
paddy@2 12 displayName: 'PaymentMethodPage',
paddy@2 13 mixins: [ScriptLoaderMixin.ReactScriptLoaderMixin, React.addons.LinkedStateMixin],
paddy@2 14 getScriptURL () {
paddy@2 15 return 'https://js.stripe.com/v2/'
paddy@2 16 },
paddy@2 17
paddy@2 18 getInitialState () {
paddy@2 19 return {
paddy@2 20 stripeLoading: true,
paddy@2 21 stripeFailedToLoad: false,
paddy@2 22 active: false,
paddy@2 23 errors: [],
paddy@2 24 number: null,
paddy@2 25 name: null,
paddy@2 26 cvc: null,
paddy@2 27 expireMonth: null,
paddy@2 28 expireYear: null,
paddy@2 29 }
paddy@2 30 },
paddy@2 31
paddy@8 32 nameValidationOutputs: {
paddy@8 33 },
paddy@8 34 numberValidationOutputs: {
paddy@8 35 'invalid_value': 'Are you sure this is right? That number didn’t work.',
paddy@8 36 'invalid_format': 'That’s not a valid credit card number.',
paddy@8 37 },
paddy@8 38 cvcValidationOutputs: {
paddy@8 39 'invalid_format': 'That’s not a valid security code.',
paddy@8 40 'invalid_value': 'That’s not the correct security code.',
paddy@8 41 },
paddy@8 42 expirationValidationOutputs: {
paddy@8 43 'invalid_format': 'That’s not a valid expiration date.',
paddy@8 44 'invalid_value': 'That card appears to be expired.',
paddy@8 45 },
paddy@8 46 balanceValidationOutputs: {
paddy@8 47 'insufficient': 'Your card was declined.',
paddy@8 48 },
paddy@8 49
paddy@8 50 catchAllValidationOutputs: {
paddy@8 51 },
paddy@2 52
paddy@2 53 onScriptLoaded () {
paddy@2 54 Stripe.setPublishableKey(config.stripeKey)
paddy@2 55 this.setState({stripeLoading: false})
paddy@2 56 },
paddy@2 57
paddy@2 58 onScriptError () {
paddy@8 59 this.setState({stripeFailedToLoad: true, errors: [{'error': 'act_of_god'}]})
paddy@2 60 },
paddy@2 61
paddy@2 62 getChargeDate () {
paddy@2 63 let created = new Date()
paddy@2 64 if (app.me && app.me.profile && app.me.profile.created && app.me.profile.created < created) {
paddy@2 65 console.log("using register date...")
paddy@2 66 created = app.me.profile.created
paddy@2 67 }
paddy@2 68 let month = created.getMonth()
paddy@2 69 let day = created.getDate()
paddy@2 70 month = month + 1
paddy@2 71 if (day > 1) {
paddy@2 72 day = 1
paddy@2 73 month = month + 1
paddy@2 74 }
paddy@2 75 let result = new Date(created.toString())
paddy@2 76 result.setDate(day)
paddy@2 77 result.setMonth(month)
paddy@2 78 return result
paddy@2 79 },
paddy@2 80
paddy@2 81 addCard (e) {
paddy@2 82 e.preventDefault()
paddy@2 83 if (this.state.stripeLoading || this.state.stripeFailedToLoad) {
paddy@2 84 return
paddy@2 85 }
paddy@2 86 this.setState({active: true})
paddy@2 87 const t = this
paddy@2 88 const errors = []
paddy@2 89 Stripe.card.createToken({
paddy@2 90 number: this.state.number,
paddy@2 91 cvc: this.state.cvc,
paddy@2 92 exp_month: this.state.expireMonth,
paddy@2 93 exp_year: this.state.expireYear,
paddy@2 94 name: this.state.name,
paddy@2 95 }, function(status, response) {
paddy@2 96 if (response.error) {
paddy@2 97 if (response.error.type == 'card_error') {
paddy@2 98 switch (response.error.code) {
paddy@2 99 case 'incorrect_number':
paddy@2 100 errors.push({'error': 'invalid_value', 'field': '/number'})
paddy@2 101 break
paddy@2 102 case 'invalid_number':
paddy@2 103 errors.push({'error': 'invalid_format', 'field': '/number'})
paddy@2 104 break
paddy@2 105 case 'invalid_expiry_month':
paddy@8 106 errors.push({'error': 'invalid_format', 'field': '/expiration'})
paddy@2 107 break
paddy@2 108 case 'invalid_expiry_year':
paddy@8 109 errors.push({'error': 'invalid_format', 'field': '/expiration'})
paddy@2 110 break
paddy@2 111 case 'invalid_cvc':
paddy@2 112 errors.push({'error': 'invalid_format', 'field': '/cvc'})
paddy@2 113 break
paddy@2 114 case 'expired_card':
paddy@2 115 errors.push({'error': 'invalid_value', 'field': '/expiration'})
paddy@2 116 break
paddy@2 117 case 'incorrect_cvc':
paddy@2 118 errors.push({'error': 'invalid_value', 'field': '/cvc'})
paddy@2 119 break
paddy@2 120 case 'incorrect_zip':
paddy@2 121 errors.push({'error': 'invalid_value', 'field': '/zip'})
paddy@2 122 break
paddy@2 123 case 'card_declined':
paddy@2 124 errors.push({'error': 'insufficient', 'field': '/balance'})
paddy@2 125 break
paddy@2 126 case 'missing':
paddy@2 127 errors.push({'error': 'missing', 'field': '/customer/card'})
paddy@2 128 break
paddy@2 129 case 'processing_error':
paddy@2 130 errors.push({'error': 'act_of_god', 'field': '/'})
paddy@2 131 break
paddy@2 132 case 'rate_limit':
paddy@2 133 errors.push({'error': 'access_denied', 'field': '/rate'})
paddy@2 134 break
paddy@2 135 default:
paddy@2 136 errors.push({'error': 'act_of_god', 'field': '/'})
paddy@2 137 break
paddy@2 138 }
paddy@2 139 } else {
paddy@2 140 console.log('Error:', response.error.message)
paddy@2 141 }
paddy@2 142 } else {
paddy@2 143 console.log('Sending '+response.id+' to server to create customer')
paddy@2 144 }
paddy@2 145 t.setState({active: false, errors: errors})
paddy@2 146 })
paddy@2 147 },
paddy@2 148
paddy@2 149 render () {
paddy@2 150 return (
paddy@2 151 <div className='container'>
paddy@2 152 <HeroUnit title='Add a Payment Method'>Gotta keep our servers online and food on the table.</HeroUnit>
paddy@2 153 <article className='onboarding payment'>
paddy@2 154 <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>
paddy@2 155 <form onSubmit={this.addCard}>
paddy@2 156 <div>
paddy@2 157 <label htmlFor='name'>Cardholder Name</label>
paddy@2 158 <input id='name' type='text' placeholder='This is the name on your card' valueLink={this.linkState('name')} />
paddy@2 159 <ValidationError errors={this.state.errors} field='/name' outputs={this.nameValidationOutputs} />
paddy@2 160
paddy@2 161 <label htmlFor='cardNumber'>Card Number</label>
paddy@2 162 <input id='cardNumber' type='text' placeholder='4242 4242 4242 4242' valueLink={this.linkState('number')} />
paddy@2 163 <ValidationError errors={this.state.errors} field='/number' outputs={this.numberValidationOutputs} />
paddy@2 164
paddy@2 165 <label htmlFor='cvc'>Security Code / CVC</label>
paddy@2 166 <input id='cvc' className='cvc' type='password' placeholder='123' valueLink={this.linkState('cvc')} />
paddy@2 167
paddy@2 168 <label htmlFor='expireMonth' className='expiration'>Expires</label>
paddy@2 169 <input id='expireMonth' className='expiration month' type='text' placeholder='01' valueLink={this.linkState('expireMonth')} />
paddy@2 170 <input id='expireYear' className='expiration year' type='text' placeholder='15' valueLink={this.linkState('expireYear')} />
paddy@2 171 <ValidationError errors={this.state.errors} field='/cvc' outputs={this.cvcValidationOutputs} />
paddy@2 172 <ValidationError errors={this.state.errors} field='/expiration' outputs={this.expirationValidationOutputs} />
paddy@8 173 <ValidationError errors={this.state.errors} field='/balance' outputs={this.balanceValidationOutputs} />
paddy@8 174 <ValidationError errors={this.state.errors} notFields={['/name', '/number', '/cvc', '/expiration', '/balance']} notParams={[]} notHeaders={[]} outputs={this.catchAllValidationOutputs} />
paddy@2 175
paddy@2 176 <div className='actionbuttons'>
paddy@2 177 <button type='button' onClick={this.skip} className='ladda-button' disabled={this.state.active}>Not Now</button>
paddy@2 178 <LaddaButton style='expand-right' active={this.state.active}>
paddy@2 179 <button type='submit' className='primary' disabled={this.state.stripeLoading || this.state.stripeFailedToLoad || this.state.active || this.state.errors.length}>Add Card</button>
paddy@2 180 </LaddaButton>
paddy@2 181 </div>
paddy@2 182 </div>
paddy@2 183 </form>
paddy@2 184 </article>
paddy@2 185 </div>
paddy@2 186 )
paddy@2 187 }
paddy@2 188 })