Merge pull request #108 from sharetribe/stripe-payment-form

Stripe payment form
This commit is contained in:
Kimmo Puputti 2017-04-07 09:22:04 +03:00 committed by GitHub
commit 2e4989ac34
7 changed files with 213 additions and 3 deletions

View file

@ -11,9 +11,18 @@
<body>
<div id="root"><!--!body--></div>
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyAIhL8ykNE8WcPihRdpkL33TXng-0SYDr0&libraries=places"></script>
<!-- Stripe script should be on every page, not just the pages that use the API:
https://stripe.com/docs/stripe.js#including-stripejs -->
<!--
Stripe script should be on every page, not just the pages that
use the API:
https://stripe.com/docs/stripe.js#including-stripejs
We need both v2 and v3. v2 is used for creating the bank
account token for providers, and v3 is used for getting
the payment token using Stripe elements.
-->
<script src="https://js.stripe.com/v2/"></script>
<script src="https://js.stripe.com/v3/"></script>
<!--!preloadedStateScript-->
</body>
</html>

View file

@ -0,0 +1,5 @@
.card {}
.submitButton {
margin-top: 1rem;
}

View file

@ -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 (
<div>
<StripePaymentForm {...this.props} onSubmit={handleSubmit} />
{this.state.token ? <p>Token: {this.state.token}</p> : null}
</div>
);
}
}
export const Empty = {
component: StripePaymentFormExample,
props: { intl: fakeIntl },
};

View file

@ -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 (
<form onSubmit={this.handleSubmit}>
<label htmlFor={css.card} />
<div
id={css.card}
ref={el => {
this.cardContainer = el;
}}
/>
{this.state.error ? <span style={{ color: 'red' }}>{this.state.error}</span> : null}
<Button className={css.submitButton} type="submit" disabled={!allowSubmit}>
<FormattedMessage id="StripePaymentForm.submitPaymentInfo" />
</Button>
</form>
);
}
}
const { func } = PropTypes;
StripePaymentForm.propTypes = {
intl: intlShape.isRequired,
onSubmit: func.isRequired,
};
export default injectIntl(StripePaymentForm);

View file

@ -23,6 +23,7 @@ import SalesConversationPage from './SalesConversationPage/SalesConversationPage
import SearchPage from './SearchPage/SearchPage';
import SecurityPage from './SecurityPage/SecurityPage';
import SignUpForm from './SignUpForm/SignUpForm';
import StripePaymentForm from './StripePaymentForm/StripePaymentForm';
import StyleguidePage from './StyleguidePage/StyleguidePage';
import Topbar from './Topbar/Topbar';
@ -52,6 +53,7 @@ export {
SearchPage,
SecurityPage,
SignUpForm,
StripePaymentForm,
StyleguidePage,
Topbar,
};

View file

@ -22,6 +22,7 @@ import * as LoginForm from './containers/LoginForm/LoginForm.example';
import * as PasswordForgottenForm
from './containers/PasswordForgottenForm/PasswordForgottenForm.example';
import * as SignUpForm from './containers/SignUpForm/SignUpForm.example';
import * as StripePaymentForm from './containers/StripePaymentForm/StripePaymentForm.example';
export {
AddImages,
@ -41,4 +42,5 @@ export {
PasswordForgottenForm,
SignUpForm,
StripeBankAccountToken,
StripePaymentForm,
};

View file

@ -36,5 +36,27 @@
"StripeBankAccountToken.createBankAccountTokenErrorIban": "Could not connect account number. Please double-check that your account number is valid in {country} when using {currency}",
"StripeBankAccountToken.invalidRoutingNumber": "Invalid routing number {number} for country {country}",
"StripeBankAccountToken.routingNumberPlaceholder": "routing number",
"StripeBankAccountToken.unsupportedCountry": "Country not supported: {country}"
"StripeBankAccountToken.unsupportedCountry": "Country not supported: {country}",
"StripePaymentForm.genericError": "Could not handle payment data. Please try again.",
"StripePaymentForm.stripe.api_connection_error": "Could not connect to Stripe API.",
"StripePaymentForm.stripe.api_error": "Error in Stripe API.",
"StripePaymentForm.stripe.authentication_error": "Could not authenticate with Stripe.",
"StripePaymentForm.stripe.card_error": "Payment card cannot be handled.",
"StripePaymentForm.stripe.invalid_request_error": "Invalid request to Stripe API.",
"StripePaymentForm.stripe.rate_limit_error": "Too many requests to Stripe API.",
"StripePaymentForm.stripe.validation_error.card_declined": "The card was declined.",
"StripePaymentForm.stripe.validation_error.expired_card": "The card has expired.",
"StripePaymentForm.stripe.validation_error.incomplete_number": "Your card number is incomplete.",
"StripePaymentForm.stripe.validation_error.incorrect_cvc": "The card's security code is incorrect.",
"StripePaymentForm.stripe.validation_error.incorrect_number": "The card number is incorrect.",
"StripePaymentForm.stripe.validation_error.incorrect_zip": "The card's zip code failed validation.",
"StripePaymentForm.stripe.validation_error.invalid_cvc": "The card's security code is invalid.",
"StripePaymentForm.stripe.validation_error.invalid_expiry_month": "The card's expiration month is invalid.",
"StripePaymentForm.stripe.validation_error.invalid_expiry_month_past": "Your card's expiration date is in the past.",
"StripePaymentForm.stripe.validation_error.invalid_expiry_year": "The card's expiration year is invalid.",
"StripePaymentForm.stripe.validation_error.invalid_number": "The card number is not a valid credit card number.",
"StripePaymentForm.stripe.validation_error.invalid_swipe_data": "The card's swipe data is invalid.",
"StripePaymentForm.stripe.validation_error.missing": "There is no card on a customer that is being charged.",
"StripePaymentForm.stripe.validation_error.processing_error": "An error occurred while processing the card.",
"StripePaymentForm.submitPaymentInfo": "Send request"
}