diff --git a/public/index.html b/public/index.html index f08a4efc..33c0b084 100644 --- a/public/index.html +++ b/public/index.html @@ -11,9 +11,18 @@
- + + diff --git a/src/containers/StripePaymentForm/StripePaymentForm.css b/src/containers/StripePaymentForm/StripePaymentForm.css new file mode 100644 index 00000000..c7be0258 --- /dev/null +++ b/src/containers/StripePaymentForm/StripePaymentForm.css @@ -0,0 +1,5 @@ +.card {} + +.submitButton { + margin-top: 1rem; +} diff --git a/src/containers/StripePaymentForm/StripePaymentForm.example.js b/src/containers/StripePaymentForm/StripePaymentForm.example.js new file mode 100644 index 00000000..603f0820 --- /dev/null +++ b/src/containers/StripePaymentForm/StripePaymentForm.example.js @@ -0,0 +1,26 @@ +import React, { Component } from 'react'; +import { fakeIntl } from '../../util/test-data'; +import StripePaymentForm from './StripePaymentForm'; + +class StripePaymentFormExample extends Component { + constructor(props) { + super(props); + this.state = { token: null }; + } + render() { + const handleSubmit = token => { + this.setState({ token }); + }; + return ( +
+ + {this.state.token ?

Token: {this.state.token}

: null} +
+ ); + } +} + +export const Empty = { + component: StripePaymentFormExample, + props: { intl: fakeIntl }, +}; diff --git a/src/containers/StripePaymentForm/StripePaymentForm.js b/src/containers/StripePaymentForm/StripePaymentForm.js new file mode 100644 index 00000000..78a3d605 --- /dev/null +++ b/src/containers/StripePaymentForm/StripePaymentForm.js @@ -0,0 +1,144 @@ +import React, { Component, PropTypes } from 'react'; +import { FormattedMessage, injectIntl, intlShape } from 'react-intl'; +import { Button } from '../../components'; +import config from '../../config'; + +import css from './StripePaymentForm.css'; + +/** + * Translate a Stripe API error object. + * + * To keep up with possible keys from the Stripe API, see: + * + * https://stripe.com/docs/api#errors + * + * Note that at least at moment, the above link doesn't list all the + * error codes that the API returns. + * + * @param {Object} intl - react-intl object from injectIntl + * @param {Object} stripeError - error object from Stripe API + * + * @return {String} translation message for the specific Stripe error, + * or the given error message (not translated) if the specific error + * type/code is not defined in the translations + * + */ +const stripeErrorTranslation = (intl, stripeError) => { + const { message, code, type } = stripeError; + + if (!code || !type) { + // Not a proper Stripe error object + return intl.formatMessage({ id: 'StripePaymentForm.genericError' }); + } + + const translationId = type === 'validation_error' + ? `StripePaymentForm.stripe.validation_error.${code}` + : `StripePaymentForm.stripe.${type}`; + + return intl.formatMessage({ + id: translationId, + defaultMessage: message, + }); +}; + +/** + * Payment form that asks for credit card info using Stripe Elements. + * + * When the card is valid and the user submits the form, a request is + * sent to the Stripe API to fetch a token that is passed to the + * onSubmit prop of this form. + * + * See: https://stripe.com/docs/elements + */ +class StripePaymentForm extends Component { + constructor(props) { + super(props); + this.state = { error: null, submitting: false, cardValueValid: false }; + this.handleCardValueChange = this.handleCardValueChange.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + } + componentDidMount() { + if (!window.Stripe) { + throw new Error('Stripe must be loaded for StripePaymentForm'); + } + this.stripe = window.Stripe(config.stripe.publishableKey); + const elements = this.stripe.elements(); + this.card = elements.create('card'); + this.card.mount(this.cardContainer); + this.card.addEventListener('change', this.handleCardValueChange); + } + componentWillUnmount() { + if (this.card) { + this.card.removeEventListener('change', this.handleCardValueChange); + } + } + handleCardValueChange(event) { + const { intl } = this.props; + const { error, complete } = event; + this.setState({ + error: error ? stripeErrorTranslation(intl, error) : null, + cardValueValid: complete, + }); + } + handleSubmit(event) { + event.preventDefault(); + + if (this.state.submitting || !this.state.cardValueValid) { + // Already submitting or card value incomplete/invalid + return; + } + + const { intl, onSubmit } = this.props; + this.setState({ submitting: true }); + + this.stripe + .createToken(this.card) + .then(result => { + const { error, token } = result; + if (error) { + this.setState({ + submitting: false, + error: stripeErrorTranslation(intl, error), + }); + } else { + this.setState({ submitting: false }); + onSubmit(token.id); + } + }) + .catch(e => { + // eslint-disable-next-line no-console + console.error(e); + this.setState({ + submitting: false, + error: stripeErrorTranslation(intl, e), + }); + }); + } + render() { + const allowSubmit = !this.state.submitting && this.state.cardValueValid; + return ( +
+