ducky/web
ducky/web/src/pages/payment.jsx
Display server error messages when adding card. When adding a credit card, correctly display user-friendly messages.
| 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 }) |