ducky/web

Paddy 2015-06-30 Parent:62e0c0df28bb Child:21f80f56cda9

9:e9e0a28a7419 Go to Latest

ducky/web/src/pages/payment.jsx

Update to use plans instead of PWYW. If we're going to lean on Stripe for most of our subscription processing, we need to use plans, instead of pay what you want. This updates the page to replace our amount input with a plan select box. It also removes the nonsense about finding your first charge date, because Stripe forced us into a simpler, but harder to predict, billing model. We also updated our CSS to work with select boxes, as well as text inputs.

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@9 27 plan: this.getDefaultPlan(),
paddy@2 28 expireMonth: null,
paddy@2 29 expireYear: null,
paddy@2 30 }
paddy@2 31 },
paddy@2 32
paddy@9 33 getDefaultPlan () {
paddy@9 34 if (app.me && app.me.subscription && app.me.subscription.plan) {
paddy@9 35 return app.me.subscription.plan
paddy@9 36 }
paddy@9 37 return 'basic_monthly'
paddy@9 38 },
paddy@9 39
paddy@8 40 nameValidationOutputs: {
paddy@8 41 },
paddy@8 42 numberValidationOutputs: {
paddy@8 43 'invalid_value': 'Are you sure this is right? That number didn’t work.',
paddy@8 44 'invalid_format': 'That’s not a valid credit card number.',
paddy@8 45 },
paddy@8 46 cvcValidationOutputs: {
paddy@8 47 'invalid_format': 'That’s not a valid security code.',
paddy@8 48 'invalid_value': 'That’s not the correct security code.',
paddy@8 49 },
paddy@8 50 expirationValidationOutputs: {
paddy@8 51 'invalid_format': 'That’s not a valid expiration date.',
paddy@8 52 'invalid_value': 'That card appears to be expired.',
paddy@8 53 },
paddy@8 54 balanceValidationOutputs: {
paddy@8 55 'insufficient': 'Your card was declined.',
paddy@8 56 },
paddy@9 57 planValidationOutputs: {
paddy@9 58 'invalid_value': 'That’s not a valid plan!',
paddy@9 59 },
paddy@8 60
paddy@8 61 catchAllValidationOutputs: {
paddy@8 62 },
paddy@2 63
paddy@2 64 onScriptLoaded () {
paddy@2 65 Stripe.setPublishableKey(config.stripeKey)
paddy@2 66 this.setState({stripeLoading: false})
paddy@2 67 },
paddy@2 68
paddy@2 69 onScriptError () {
paddy@8 70 this.setState({stripeFailedToLoad: true, errors: [{'error': 'act_of_god'}]})
paddy@2 71 },
paddy@2 72
paddy@2 73 addCard (e) {
paddy@2 74 e.preventDefault()
paddy@2 75 if (this.state.stripeLoading || this.state.stripeFailedToLoad) {
paddy@2 76 return
paddy@2 77 }
paddy@2 78 this.setState({active: true})
paddy@2 79 const t = this
paddy@2 80 const errors = []
paddy@2 81 Stripe.card.createToken({
paddy@2 82 number: this.state.number,
paddy@2 83 cvc: this.state.cvc,
paddy@2 84 exp_month: this.state.expireMonth,
paddy@2 85 exp_year: this.state.expireYear,
paddy@2 86 name: this.state.name,
paddy@2 87 }, function(status, response) {
paddy@2 88 if (response.error) {
paddy@2 89 if (response.error.type == 'card_error') {
paddy@2 90 switch (response.error.code) {
paddy@2 91 case 'incorrect_number':
paddy@2 92 errors.push({'error': 'invalid_value', 'field': '/number'})
paddy@2 93 break
paddy@2 94 case 'invalid_number':
paddy@2 95 errors.push({'error': 'invalid_format', 'field': '/number'})
paddy@2 96 break
paddy@2 97 case 'invalid_expiry_month':
paddy@8 98 errors.push({'error': 'invalid_format', 'field': '/expiration'})
paddy@2 99 break
paddy@2 100 case 'invalid_expiry_year':
paddy@8 101 errors.push({'error': 'invalid_format', 'field': '/expiration'})
paddy@2 102 break
paddy@2 103 case 'invalid_cvc':
paddy@2 104 errors.push({'error': 'invalid_format', 'field': '/cvc'})
paddy@2 105 break
paddy@2 106 case 'expired_card':
paddy@2 107 errors.push({'error': 'invalid_value', 'field': '/expiration'})
paddy@2 108 break
paddy@2 109 case 'incorrect_cvc':
paddy@2 110 errors.push({'error': 'invalid_value', 'field': '/cvc'})
paddy@2 111 break
paddy@2 112 case 'incorrect_zip':
paddy@2 113 errors.push({'error': 'invalid_value', 'field': '/zip'})
paddy@2 114 break
paddy@2 115 case 'card_declined':
paddy@2 116 errors.push({'error': 'insufficient', 'field': '/balance'})
paddy@2 117 break
paddy@2 118 case 'missing':
paddy@2 119 errors.push({'error': 'missing', 'field': '/customer/card'})
paddy@2 120 break
paddy@2 121 case 'processing_error':
paddy@2 122 errors.push({'error': 'act_of_god', 'field': '/'})
paddy@2 123 break
paddy@2 124 case 'rate_limit':
paddy@2 125 errors.push({'error': 'access_denied', 'field': '/rate'})
paddy@2 126 break
paddy@2 127 default:
paddy@2 128 errors.push({'error': 'act_of_god', 'field': '/'})
paddy@2 129 break
paddy@2 130 }
paddy@2 131 } else {
paddy@2 132 console.log('Error:', response.error.message)
paddy@2 133 }
paddy@2 134 } else {
paddy@2 135 console.log('Sending '+response.id+' to server to create customer')
paddy@2 136 }
paddy@2 137 t.setState({active: false, errors: errors})
paddy@2 138 })
paddy@2 139 },
paddy@2 140
paddy@2 141 render () {
paddy@2 142 return (
paddy@2 143 <div className='container'>
paddy@9 144 <HeroUnit title='Set Up Your Subscription'>Gotta pay those bills.</HeroUnit>
paddy@2 145 <article className='onboarding payment'>
paddy@9 146 <p>Ducky costs money to run, and to keep it improving. We pass these costs on to you. There’s no parent company, no ads, nothing but people making software they want you to enjoy. Also, you get a
paddy@9 147 free 31 day trial. Cancel before it ends, and we won’t charge you at all.</p>
paddy@2 148 <form onSubmit={this.addCard}>
paddy@2 149 <div>
paddy@2 150 <label htmlFor='name'>Cardholder Name</label>
paddy@2 151 <input id='name' type='text' placeholder='This is the name on your card' valueLink={this.linkState('name')} />
paddy@2 152 <ValidationError errors={this.state.errors} field='/name' outputs={this.nameValidationOutputs} />
paddy@2 153
paddy@2 154 <label htmlFor='cardNumber'>Card Number</label>
paddy@2 155 <input id='cardNumber' type='text' placeholder='4242 4242 4242 4242' valueLink={this.linkState('number')} />
paddy@2 156 <ValidationError errors={this.state.errors} field='/number' outputs={this.numberValidationOutputs} />
paddy@2 157
paddy@2 158 <label htmlFor='cvc'>Security Code / CVC</label>
paddy@2 159 <input id='cvc' className='cvc' type='password' placeholder='123' valueLink={this.linkState('cvc')} />
paddy@2 160
paddy@2 161 <label htmlFor='expireMonth' className='expiration'>Expires</label>
paddy@2 162 <input id='expireMonth' className='expiration month' type='text' placeholder='01' valueLink={this.linkState('expireMonth')} />
paddy@2 163 <input id='expireYear' className='expiration year' type='text' placeholder='15' valueLink={this.linkState('expireYear')} />
paddy@2 164 <ValidationError errors={this.state.errors} field='/cvc' outputs={this.cvcValidationOutputs} />
paddy@2 165 <ValidationError errors={this.state.errors} field='/expiration' outputs={this.expirationValidationOutputs} />
paddy@9 166
paddy@9 167 <label htmlFor='plan'>Plan</label>
paddy@9 168 <select id='plan' className='plan' defaultValue={this.getDefaultPlan()}>
paddy@9 169 <option value='basic_monthly'>Basic Monthly - $2/month</option>
paddy@9 170 <option value='basic_yearly'>Basic Yearly - $20/year</option>
paddy@9 171 <option value='supporter_monthly'>Supporter Monthly - $5/month</option>
paddy@9 172 <option value='supporter_yearly'>Supporter Yearly - $50/year</option>
paddy@9 173 </select>
paddy@9 174 <ValidationError errors={this.state.errors} field='/plan' outputs={this.planValidationOutputs} />
paddy@9 175 <p>There’s no difference between the Supporter and Basic tiers. The Supporter tier is just for people who love Ducky and want to see it grow and improve.</p>
paddy@9 176
paddy@8 177 <ValidationError errors={this.state.errors} field='/balance' outputs={this.balanceValidationOutputs} />
paddy@9 178 <ValidationError errors={this.state.errors} notFields={['/name', '/number', '/cvc', '/expiration', '/plan', '/balance']} notParams={[]} notHeaders={[]} outputs={this.catchAllValidationOutputs} />
paddy@2 179
paddy@2 180 <div className='actionbuttons'>
paddy@2 181 <LaddaButton style='expand-right' active={this.state.active}>
paddy@2 182 <button type='submit' className='primary' disabled={this.state.stripeLoading || this.state.stripeFailedToLoad || this.state.active || this.state.errors.length}>Add Card</button>
paddy@2 183 </LaddaButton>
paddy@2 184 </div>
paddy@2 185 </div>
paddy@2 186 </form>
paddy@2 187 </article>
paddy@2 188 </div>
paddy@2 189 )
paddy@2 190 }
paddy@2 191 })