ducky/web
ducky/web/src/pages/payment.jsx
Enable catch-all in our ValidationError component. We're doing this an ugly, hacky way. But it works, and right now, that's what counts. To match our params/fields/headers properties on the ValidationError component, we're going to add the notParams/notFields/notHeaders properties--they match any error _not_ targeting those params/fields/headers. Basically, "any error that wouldn't be caught by these filters". Which is an ugly, but workable, solution for a catch-all ValidationError--just tell it to catch anything but the params/fields/headers that are being handled by the other ValidationErrors. Our implementation of this in the RegisterPage component validates (ha!) that it's at least workable model, if not overly pretty. Also, I anticipate some human error bugs in the future, where one of the field-specific ValidationErrors gets updated and the catch-all ValidationError does not. But whatever. For now, this is Good Enough™.
| 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@2 | 32 nameValidationOutputs: {}, |
| paddy@2 | 33 numberValidationOutputs: {}, |
| paddy@2 | 34 cvcValidationOutputs: {}, |
| paddy@2 | 35 expirationValidationOutputs: {}, |
| paddy@2 | 36 |
| paddy@2 | 37 onScriptLoaded () { |
| paddy@2 | 38 Stripe.setPublishableKey(config.stripeKey) |
| paddy@2 | 39 this.setState({stripeLoading: false}) |
| paddy@2 | 40 }, |
| paddy@2 | 41 |
| paddy@2 | 42 onScriptError () { |
| paddy@2 | 43 this.setState({stripeFailedToLoad: true}) |
| paddy@2 | 44 }, |
| paddy@2 | 45 |
| paddy@2 | 46 getChargeDate () { |
| paddy@2 | 47 let created = new Date() |
| paddy@2 | 48 if (app.me && app.me.profile && app.me.profile.created && app.me.profile.created < created) { |
| paddy@2 | 49 console.log("using register date...") |
| paddy@2 | 50 created = app.me.profile.created |
| paddy@2 | 51 } |
| paddy@2 | 52 let month = created.getMonth() |
| paddy@2 | 53 let day = created.getDate() |
| paddy@2 | 54 month = month + 1 |
| paddy@2 | 55 if (day > 1) { |
| paddy@2 | 56 day = 1 |
| paddy@2 | 57 month = month + 1 |
| paddy@2 | 58 } |
| paddy@2 | 59 let result = new Date(created.toString()) |
| paddy@2 | 60 result.setDate(day) |
| paddy@2 | 61 result.setMonth(month) |
| paddy@2 | 62 return result |
| paddy@2 | 63 }, |
| paddy@2 | 64 |
| paddy@2 | 65 addCard (e) { |
| paddy@2 | 66 e.preventDefault() |
| paddy@2 | 67 if (this.state.stripeLoading || this.state.stripeFailedToLoad) { |
| paddy@2 | 68 return |
| paddy@2 | 69 } |
| paddy@2 | 70 this.setState({active: true}) |
| paddy@2 | 71 const t = this |
| paddy@2 | 72 const errors = [] |
| paddy@2 | 73 Stripe.card.createToken({ |
| paddy@2 | 74 number: this.state.number, |
| paddy@2 | 75 cvc: this.state.cvc, |
| paddy@2 | 76 exp_month: this.state.expireMonth, |
| paddy@2 | 77 exp_year: this.state.expireYear, |
| paddy@2 | 78 name: this.state.name, |
| paddy@2 | 79 }, function(status, response) { |
| paddy@2 | 80 console.log(status) |
| paddy@2 | 81 console.log(response) |
| paddy@2 | 82 if (response.error) { |
| paddy@2 | 83 if (response.error.type == 'card_error') { |
| paddy@2 | 84 switch (response.error.code) { |
| paddy@2 | 85 case 'incorrect_number': |
| paddy@2 | 86 errors.push({'error': 'invalid_value', 'field': '/number'}) |
| paddy@2 | 87 break |
| paddy@2 | 88 case 'invalid_number': |
| paddy@2 | 89 errors.push({'error': 'invalid_format', 'field': '/number'}) |
| paddy@2 | 90 break |
| paddy@2 | 91 case 'invalid_expiry_month': |
| paddy@2 | 92 errors.push({'error': 'invalid_format', 'field': '/expireMonth'}) |
| paddy@2 | 93 break |
| paddy@2 | 94 case 'invalid_expiry_year': |
| paddy@2 | 95 errors.push({'error': 'invalid_format', 'field': '/expireYear'}) |
| paddy@2 | 96 break |
| paddy@2 | 97 case 'invalid_cvc': |
| paddy@2 | 98 errors.push({'error': 'invalid_format', 'field': '/cvc'}) |
| paddy@2 | 99 break |
| paddy@2 | 100 case 'expired_card': |
| paddy@2 | 101 errors.push({'error': 'invalid_value', 'field': '/expiration'}) |
| paddy@2 | 102 break |
| paddy@2 | 103 case 'incorrect_cvc': |
| paddy@2 | 104 errors.push({'error': 'invalid_value', 'field': '/cvc'}) |
| paddy@2 | 105 break |
| paddy@2 | 106 case 'incorrect_zip': |
| paddy@2 | 107 errors.push({'error': 'invalid_value', 'field': '/zip'}) |
| paddy@2 | 108 break |
| paddy@2 | 109 case 'card_declined': |
| paddy@2 | 110 errors.push({'error': 'insufficient', 'field': '/balance'}) |
| paddy@2 | 111 break |
| paddy@2 | 112 case 'missing': |
| paddy@2 | 113 errors.push({'error': 'missing', 'field': '/customer/card'}) |
| paddy@2 | 114 break |
| paddy@2 | 115 case 'processing_error': |
| paddy@2 | 116 errors.push({'error': 'act_of_god', 'field': '/'}) |
| paddy@2 | 117 break |
| paddy@2 | 118 case 'rate_limit': |
| paddy@2 | 119 errors.push({'error': 'access_denied', 'field': '/rate'}) |
| paddy@2 | 120 break |
| paddy@2 | 121 default: |
| paddy@2 | 122 errors.push({'error': 'act_of_god', 'field': '/'}) |
| paddy@2 | 123 break |
| paddy@2 | 124 } |
| paddy@2 | 125 } else { |
| paddy@2 | 126 console.log('Error:', response.error.message) |
| paddy@2 | 127 } |
| paddy@2 | 128 } else { |
| paddy@2 | 129 console.log('Sending '+response.id+' to server to create customer') |
| paddy@2 | 130 } |
| paddy@2 | 131 t.setState({active: false, errors: errors}) |
| paddy@2 | 132 }) |
| paddy@2 | 133 }, |
| paddy@2 | 134 |
| paddy@2 | 135 render () { |
| paddy@2 | 136 return ( |
| paddy@2 | 137 <div className='container'> |
| paddy@2 | 138 <HeroUnit title='Add a Payment Method'>Gotta keep our servers online and food on the table.</HeroUnit> |
| paddy@2 | 139 <article className='onboarding payment'> |
| paddy@2 | 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> |
| paddy@2 | 141 <form onSubmit={this.addCard}> |
| paddy@2 | 142 <div> |
| paddy@2 | 143 <label htmlFor='name'>Cardholder Name</label> |
| paddy@2 | 144 <input id='name' type='text' placeholder='This is the name on your card' valueLink={this.linkState('name')} /> |
| paddy@2 | 145 <ValidationError errors={this.state.errors} field='/name' outputs={this.nameValidationOutputs} /> |
| paddy@2 | 146 |
| paddy@2 | 147 <label htmlFor='cardNumber'>Card Number</label> |
| paddy@2 | 148 <input id='cardNumber' type='text' placeholder='4242 4242 4242 4242' valueLink={this.linkState('number')} /> |
| paddy@2 | 149 <ValidationError errors={this.state.errors} field='/number' outputs={this.numberValidationOutputs} /> |
| paddy@2 | 150 |
| paddy@2 | 151 <label htmlFor='cvc'>Security Code / CVC</label> |
| paddy@2 | 152 <input id='cvc' className='cvc' type='password' placeholder='123' valueLink={this.linkState('cvc')} /> |
| paddy@2 | 153 |
| paddy@2 | 154 <label htmlFor='expireMonth' className='expiration'>Expires</label> |
| paddy@2 | 155 <input id='expireMonth' className='expiration month' type='text' placeholder='01' valueLink={this.linkState('expireMonth')} /> |
| paddy@2 | 156 <input id='expireYear' className='expiration year' type='text' placeholder='15' valueLink={this.linkState('expireYear')} /> |
| paddy@2 | 157 <ValidationError errors={this.state.errors} field='/cvc' outputs={this.cvcValidationOutputs} /> |
| paddy@2 | 158 <ValidationError errors={this.state.errors} field='/expiration' outputs={this.expirationValidationOutputs} /> |
| paddy@2 | 159 |
| paddy@2 | 160 <div className='actionbuttons'> |
| paddy@2 | 161 <button type='button' onClick={this.skip} className='ladda-button' disabled={this.state.active}>Not Now</button> |
| paddy@2 | 162 <LaddaButton style='expand-right' active={this.state.active}> |
| paddy@2 | 163 <button type='submit' className='primary' disabled={this.state.stripeLoading || this.state.stripeFailedToLoad || this.state.active || this.state.errors.length}>Add Card</button> |
| paddy@2 | 164 </LaddaButton> |
| paddy@2 | 165 </div> |
| paddy@2 | 166 </div> |
| paddy@2 | 167 </form> |
| paddy@2 | 168 </article> |
| paddy@2 | 169 </div> |
| paddy@2 | 170 ) |
| paddy@2 | 171 } |
| paddy@2 | 172 }) |