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