2016-11-07 20:24:38 +00:00
|
|
|
|
import React, {Component} from 'react';
|
|
|
|
|
import Link from '../base/Link';
|
|
|
|
|
import Nl2Br from '../Nl2Br';
|
|
|
|
|
import Modal from '../base/Modal';
|
|
|
|
|
import ModalBody from '../base/ModalBody';
|
|
|
|
|
import ModalHeader from '../base/ModalHeader';
|
|
|
|
|
import ModalFooter from '../base/ModalFooter';
|
2016-11-09 03:18:25 +00:00
|
|
|
|
import * as session from '../../../sync/session';
|
2016-11-07 20:24:38 +00:00
|
|
|
|
|
|
|
|
|
const MONTHS = [
|
|
|
|
|
{name: 'January', value: '01'},
|
|
|
|
|
{name: 'February', value: '02'},
|
|
|
|
|
{name: 'March', value: '03'},
|
|
|
|
|
{name: 'April', value: '04'},
|
|
|
|
|
{name: 'May', value: '05'},
|
|
|
|
|
{name: 'June', value: '06'},
|
|
|
|
|
{name: 'July', value: '07'},
|
|
|
|
|
{name: 'August', value: '08'},
|
|
|
|
|
{name: 'September', value: '09'},
|
|
|
|
|
{name: 'October', value: '10'},
|
|
|
|
|
{name: 'November', value: '11'},
|
|
|
|
|
{name: 'December', value: '12'},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const YEARS = [];
|
|
|
|
|
for (let i = 0; i < 20; i++) {
|
|
|
|
|
YEARS.push(2016 + i);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class PaymentModal extends Component {
|
2016-11-26 08:29:16 +00:00
|
|
|
|
state = {
|
|
|
|
|
title: '',
|
|
|
|
|
message: '',
|
|
|
|
|
error: '',
|
|
|
|
|
loading: false,
|
|
|
|
|
cardType: '',
|
|
|
|
|
selectedPlan: 'plus-monthly-1'
|
|
|
|
|
};
|
2016-11-07 20:24:38 +00:00
|
|
|
|
|
2016-11-17 23:22:23 +00:00
|
|
|
|
show (options = {}) {
|
|
|
|
|
const {message, title} = options;
|
2016-11-07 20:24:38 +00:00
|
|
|
|
this.setState({error: '', message, title});
|
|
|
|
|
this.modal.show();
|
|
|
|
|
setTimeout(() => this._nameInput.focus(), 100);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hide () {
|
|
|
|
|
this.modal.hide();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_handlePlanChange (selectedPlan) {
|
|
|
|
|
this.setState({selectedPlan});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_handleCreditCardCvcChange (e) {
|
|
|
|
|
const value = e.target.value.trim();
|
|
|
|
|
if (!value) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
e.target.value = value.replace(/[^0-9]/g, '').trim()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_handleCreditCardNumberChange (e) {
|
|
|
|
|
const value = e.target.value.trim();
|
|
|
|
|
if (!value) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2016-11-17 21:22:19 +00:00
|
|
|
|
const cardType = Stripe.card.cardType(value);
|
2016-11-07 20:24:38 +00:00
|
|
|
|
const lastChar = value[e.target.value.length - 1];
|
|
|
|
|
const num = value.replace(/[^0-9]*/g, '');
|
2016-11-17 21:22:19 +00:00
|
|
|
|
let newNum = '';
|
|
|
|
|
|
|
|
|
|
if (cardType === 'American Express') {
|
|
|
|
|
// 1111 222222 33333
|
|
|
|
|
const g1 = num.slice(0, 4);
|
|
|
|
|
const g2 = num.slice(4, 10);
|
|
|
|
|
const g3 = num.slice(10, 15);
|
|
|
|
|
|
|
|
|
|
newNum = g1;
|
|
|
|
|
newNum += g2 ? ` ${g2}` : '';
|
|
|
|
|
newNum += g3 ? ` ${g3}` : '';
|
|
|
|
|
} else if (cardType === 'Diners Club') {
|
|
|
|
|
// 1111 2222 3333 44
|
|
|
|
|
const g1 = num.slice(0, 4);
|
|
|
|
|
const g2 = num.slice(4, 8);
|
|
|
|
|
const g3 = num.slice(8, 12);
|
|
|
|
|
const g4 = num.slice(12, 14);
|
|
|
|
|
|
|
|
|
|
newNum = g1;
|
|
|
|
|
newNum += g2 ? ` ${g2}` : '';
|
|
|
|
|
newNum += g3 ? ` ${g3}` : '';
|
|
|
|
|
newNum += g4 ? ` ${g4}` : '';
|
|
|
|
|
} else {
|
|
|
|
|
// 1111 2222 3333 4444
|
|
|
|
|
const g1 = num.slice(0, 4);
|
|
|
|
|
const g2 = num.slice(4, 8);
|
|
|
|
|
const g3 = num.slice(8, 12);
|
|
|
|
|
const g4 = num.slice(12, 16);
|
|
|
|
|
|
|
|
|
|
newNum = g1;
|
|
|
|
|
newNum += g2 ? ` ${g2}` : '';
|
|
|
|
|
newNum += g3 ? ` ${g3}` : '';
|
|
|
|
|
newNum += g4 ? ` ${g4}` : '';
|
|
|
|
|
}
|
2016-11-07 20:24:38 +00:00
|
|
|
|
|
|
|
|
|
// Handle trailing dash so we can add and delete dashes properly
|
2016-11-17 21:22:19 +00:00
|
|
|
|
if (lastChar === ' ') {
|
|
|
|
|
newNum += ' ';
|
2016-11-07 20:24:38 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.setState({cardType: cardType === 'Unknown' ? '' : cardType});
|
|
|
|
|
|
2016-11-17 21:22:19 +00:00
|
|
|
|
// Only update number if it changed from the user's original to prevent cursor jump
|
|
|
|
|
if (newNum !== value) {
|
|
|
|
|
e.target.value = newNum;
|
|
|
|
|
}
|
2016-11-07 20:24:38 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_handleSubmit (e) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
|
|
this.setState({error: '', loading: true});
|
|
|
|
|
|
|
|
|
|
const params = {
|
|
|
|
|
name: this._nameInput.value,
|
2016-11-17 21:22:19 +00:00
|
|
|
|
number: this._cardNumberInput.value.replace(/ /g, ''),
|
2016-11-07 20:24:38 +00:00
|
|
|
|
cvc: this._cvcInput.value,
|
|
|
|
|
exp_month: parseInt(this._expiryMonthInput.value, 10),
|
|
|
|
|
exp_year: parseInt(this._expiryYearInput.value, 10),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Stripe.card.createToken(params, async (status, response) => {
|
|
|
|
|
if (status === 200) {
|
|
|
|
|
try {
|
|
|
|
|
await session.subscribe(response.id, this.state.selectedPlan);
|
|
|
|
|
this.setState({error: '', loading: false});
|
|
|
|
|
this.hide();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
this.setState({error: e.message, loading: false});
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
this.setState({error: response.error.message, loading: false});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
render () {
|
|
|
|
|
const {selectedPlan, cardType, message, title} = this.state;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<form onSubmit={this._handleSubmit.bind(this)}
|
|
|
|
|
key={session.getCurrentSessionId()}>
|
|
|
|
|
<Modal ref={m => this.modal = m} {...this.props}>
|
|
|
|
|
<ModalHeader>{title || "Enter Payment Method"}</ModalHeader>
|
|
|
|
|
<ModalBody className="pad changelog">
|
|
|
|
|
<div style={{maxWidth: '32rem', margin: 'auto'}}>
|
|
|
|
|
{message ? (
|
|
|
|
|
<Nl2Br className="notice info">{message}</Nl2Br>
|
|
|
|
|
) : null}
|
|
|
|
|
<div>
|
2016-11-27 21:42:38 +00:00
|
|
|
|
<div className="pad-left-sm">
|
2016-11-07 20:24:38 +00:00
|
|
|
|
<div className="inline-block text-center"
|
|
|
|
|
style={{width: '50%'}}>
|
|
|
|
|
<input
|
|
|
|
|
id="plan-plus-monthly-1"
|
|
|
|
|
type="radio"
|
|
|
|
|
name="payment-cycle"
|
|
|
|
|
checked={selectedPlan === 'plus-monthly-1'}
|
|
|
|
|
onChange={e => this._handlePlanChange('plus-monthly-1')}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<label htmlFor="plan-plus-monthly-1">
|
|
|
|
|
Per Month ($5/mo)
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="inline-block text-center"
|
|
|
|
|
style={{width: '50%'}}>
|
|
|
|
|
<input
|
|
|
|
|
id="plan-plus-yearly-1"
|
|
|
|
|
type="radio"
|
|
|
|
|
name="payment-cycle"
|
|
|
|
|
checked={selectedPlan === 'plus-yearly-1'}
|
|
|
|
|
onChange={e => this._handlePlanChange('plus-yearly-1')}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<label htmlFor="plan-plus-yearly-1">Annual ($50/yr)</label>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="pad-top">
|
|
|
|
|
<label htmlFor="payment-name">Full name</label>
|
|
|
|
|
<div className="form-control form-control--outlined">
|
|
|
|
|
<input type="text"
|
|
|
|
|
required="required"
|
|
|
|
|
id="payment-name"
|
|
|
|
|
name="payment-name"
|
|
|
|
|
placeholder="Paula Jones"
|
|
|
|
|
defaultValue={session.getFullName()}
|
|
|
|
|
ref={n => this._nameInput = n}/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label htmlFor="payment-card-number">
|
|
|
|
|
Card Number {cardType ? `(${cardType})` : ''}
|
|
|
|
|
</label>
|
|
|
|
|
<div className="form-control form-control--outlined">
|
|
|
|
|
<input type="text"
|
|
|
|
|
required="required"
|
|
|
|
|
id="payment-card-number"
|
2016-11-17 21:22:19 +00:00
|
|
|
|
placeholder="4012 8888 8888 1881"
|
2016-11-07 20:24:38 +00:00
|
|
|
|
onChange={this._handleCreditCardNumberChange.bind(this)}
|
|
|
|
|
ref={n => this._cardNumberInput = n}/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<div className="inline-block" style={{width: '66%'}}>
|
|
|
|
|
<label htmlFor="payment-expiry">
|
|
|
|
|
Expiration Date
|
|
|
|
|
</label>
|
2016-11-27 21:42:38 +00:00
|
|
|
|
<div className="pad-left-sm pad-top-sm">
|
2016-11-07 20:24:38 +00:00
|
|
|
|
<select name="payment-expiration-month"
|
|
|
|
|
id="payment-expiration-month"
|
|
|
|
|
ref={n => this._expiryMonthInput = n}>
|
|
|
|
|
{MONTHS.map(({name, value}) => (
|
|
|
|
|
<option key={value} value={value}>
|
|
|
|
|
{value} – {name}</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
{" "}
|
|
|
|
|
<select name="payment-expiration-year"
|
|
|
|
|
id="payment-expiration-year"
|
|
|
|
|
ref={n => this._expiryYearInput = n}>
|
|
|
|
|
{YEARS.map(year => (
|
|
|
|
|
<option key={year} value={year}>{year}</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="inline-block" style={{width: '34%'}}>
|
|
|
|
|
<label htmlFor="payment-cvc">Security Code (CVC)</label>
|
|
|
|
|
<div className="form-control form-control--outlined">
|
|
|
|
|
<input type="text"
|
|
|
|
|
maxLength="4"
|
|
|
|
|
required="required"
|
|
|
|
|
id="payment-cvc"
|
|
|
|
|
name="payment-cvc"
|
|
|
|
|
placeholder="013"
|
|
|
|
|
onChange={this._handleCreditCardCvcChange.bind(this)}
|
|
|
|
|
ref={n => this._cvcInput = n}/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{this.state.error ? (
|
|
|
|
|
<div className="danger pad-top">** {this.state.error}</div>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
</ModalBody>
|
|
|
|
|
<ModalFooter>
|
|
|
|
|
<div className="margin-left faint italic txt-sm">
|
|
|
|
|
* Payments secured and managed by
|
|
|
|
|
{" "}
|
|
|
|
|
<Link href="https://stripe.com">Stripe</Link>
|
|
|
|
|
</div>
|
|
|
|
|
{this.state.loading ? (
|
|
|
|
|
<button type="submit" className="btn">
|
|
|
|
|
<i className="fa fa-spin fa-refresh margin-right-sm"></i>
|
|
|
|
|
{" "}
|
|
|
|
|
Processing
|
|
|
|
|
</button>
|
|
|
|
|
) : (
|
|
|
|
|
<button type="submit" className="btn">
|
|
|
|
|
Submit Details
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</ModalFooter>
|
|
|
|
|
</Modal>
|
|
|
|
|
</form>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PaymentModal.propTypes = {};
|
|
|
|
|
|
|
|
|
|
export default PaymentModal;
|