UNPKG

react-stripe-checkout

Version:

Easily inject checkout.js as a react component. Will load the script on demand and supports all the options from stripe docs.

495 lines (430 loc) 15.3 kB
import React from 'react'; import PropTypes from 'prop-types'; let scriptLoading = false; let scriptLoaded = false; let scriptDidError = false; export default class ReactStripeCheckout extends React.Component { static defaultProps = { className: 'StripeCheckout', label: 'Pay With Card', locale: 'auto', ComponentClass: 'span', reconfigureOnUpdate: false, triggerEvent: 'onClick', } static propTypes = { // Opens / closes the checkout modal by value // WARNING: does not work on mobile due to browser security restrictions // NOTE: Must be set to false when receiving token to prevent modal from // opening automatically after closing desktopShowModal: PropTypes.bool, triggerEvent: PropTypes.oneOf([ 'onClick', 'onTouchTap', 'onTouchStart', ]), // If included, will render the default blue button with label text. // (Requires including stripe-checkout.css or adding the .styl file // to your pipeline) label: PropTypes.string, // Custom styling for default button style: PropTypes.object, // Custom styling for <span> tag inside default button textStyle: PropTypes.object, // Prevents any events from opening the popup // Adds the disabled prop to the button and adjusts the styling as well disabled: PropTypes.bool, // Named component to wrap button (eg. div) ComponentClass: PropTypes.string, // Show a loading indicator showLoadingDialog: PropTypes.func, // Hide the loading indicator hideLoadingDialog: PropTypes.func, // Run this method when the scrupt fails to load. Will run if the internet // connection is offline when attemting to load the script. onScriptError: PropTypes.func, // Runs when the script tag is created, but before it is added to the DOM onScriptTagCreated: PropTypes.func, // By default, any time the React component is updated, it will call // StripeCheckout.configure, which may result in additional XHR calls to the // stripe API. If you know the first configuration is all you need, you // can set this to false. Subsequent updates will affect the StripeCheckout.open // (e.g. different prices) reconfigureOnUpdate: PropTypes.bool, // ===================================================== // Required by stripe // see Stripe docs for more info: // https://stripe.com/docs/checkout#integration-custom // ===================================================== // Your publishable key (test or live). // can't use "key" as a prop in react, so have to change the keyname stripeKey: PropTypes.string.isRequired, // The callback to invoke when the Checkout process is complete. // function(token) // token is the token object created. // token.id can be used to create a charge or customer. // token.email contains the email address entered by the user. token: PropTypes.func.isRequired, // ========================== // Highly Recommended Options // ========================== // Name of the company or website. name: PropTypes.string, // A description of the product or service being purchased. description: PropTypes.string, // A relative URL pointing to a square image of your brand or product. The // recommended minimum size is 128x128px. The recommended image types are // .gif, .jpeg, and .png. image: PropTypes.string, // The amount (in cents) that's shown to the user. Note that you will still // have to explicitly include it when you create a charge using the API. amount: PropTypes.number, // Specify auto to display Checkout in the user's preferred language, if // available. English will be used by default. // // https://stripe.com/docs/checkout#supported-languages // for more info. locale: PropTypes.oneOf([ 'auto', // (Default) Automatically chosen by checkout 'zh', // Simplified Chinese 'da', // Danish 'nl', // Dutch 'en', // English 'fr', // French 'de', // German 'it', // Italian 'ja', // Japanease 'no', // Norwegian 'es', // Spanish 'sv', // Swedish ]), // ============== // Optional Props // ============== // The currency of the amount (3-letter ISO code). The default is USD. currency: PropTypes.oneOf([ 'AED','AFN','ALL','AMD','ANG','AOA','ARS','AUD','AWG','AZN','BAM','BBD', // eslint-disable-line comma-spacing 'BDT','BGN','BIF','BMD','BND','BOB','BRL','BSD','BWP','BZD','CAD','CDF', // eslint-disable-line comma-spacing 'CHF','CLP','CNY','COP','CRC','CVE','CZK','DJF','DKK','DOP','DZD','EEK', // eslint-disable-line comma-spacing 'EGP','ETB','EUR','FJD','FKP','GBP','GEL','GIP','GMD','GNF','GTQ','GYD', // eslint-disable-line comma-spacing 'HKD','HNL','HRK','HTG','HUF','IDR','ILS','INR','ISK','JMD','JPY','KES', // eslint-disable-line comma-spacing 'KGS','KHR','KMF','KRW','KYD','KZT','LAK','LBP','LKR','LRD','LSL','LTL', // eslint-disable-line comma-spacing 'LVL','MAD','MDL','MGA','MKD','MNT','MOP','MRO','MUR','MVR','MWK','MXN', // eslint-disable-line comma-spacing 'MYR','MZN','NAD','NGN','NIO','NOK','NPR','NZD','PAB','PEN','PGK','PHP', // eslint-disable-line comma-spacing 'PKR','PLN','PYG','QAR','RON','RSD','RUB','RWF','SAR','SBD','SCR','SEK', // eslint-disable-line comma-spacing 'SGD','SHP','SLL','SOS','SRD','STD','SVC','SZL','THB','TJS','TOP','TRY', // eslint-disable-line comma-spacing 'TTD','TWD','TZS','UAH','UGX','USD','UYU','UZS','VND','VUV','WST','XAF', // eslint-disable-line comma-spacing 'XCD','XOF','XPF','YER','ZAR','ZMW', // eslint-disable-line comma-spacing ]), // The label of the payment button in the Checkout form (e.g. “Subscribe”, // “Pay {{amount}}”, etc.). If you include {{amount}}, it will be replaced // by the provided amount. Otherwise, the amount will be appended to the // end of your label. panelLabel: PropTypes.string, // Specify whether Checkout should validate the billing ZIP code (true or // false) zipCode: PropTypes.bool, // Specify whether Checkout should collect the user's billing address // (true or false). The default is false. billingAddress: PropTypes.bool, // Specify whether Checkout should collect the user's shipping address // (true or false). The default is false. shippingAddress: PropTypes.bool, // Specify whether Checkout should validate the billing ZIP code (true or // false). The default is false. email: PropTypes.string, // Specify whether to include the option to "Remember Me" for future // purchases (true or false). The default is true. allowRememberMe: PropTypes.bool, // Specify whether to accept Bitcoin in Checkout. The default is false. bitcoin: PropTypes.bool, // Specify whether to accept Alipay ('auto', true, or false). The default // is false. alipay: PropTypes.oneOf(['auto', true, false]), // Specify if you need reusable access to the customer's Alipay account // (true or false). The default is false. alipayReusable: PropTypes.bool, // function() The callback to invoke when Checkout is opened (not supported // in IE6 and IE7). opened: PropTypes.func, // function() The callback to invoke when Checkout is closed (not supported // in IE6 and IE7). closed: PropTypes.func, } static _isMounted = false; constructor(props) { super(props); this.state = { open: false, buttonActive: false, }; } componentDidMount() { this._isMounted = true; if (scriptLoaded) { return; } if (scriptLoading) { return; } scriptLoading = true; const script = document.createElement('script'); if (typeof this.props.onScriptTagCreated === 'function') { this.props.onScriptTagCreated(script); } script.src = 'https://checkout.stripe.com/checkout.js'; script.async = 1; this.loadPromise = (() => { let canceled = false; const promise = new Promise((resolve, reject) => { script.onload = () => { scriptLoaded = true; scriptLoading = false; resolve(); this.onScriptLoaded(); }; script.onerror = (event) => { scriptDidError = true; scriptLoading = false; reject(event); this.onScriptError(event); }; }); const wrappedPromise = new Promise((accept, cancel) => { promise.then(() => canceled ? cancel({ isCanceled: true }) : accept()); // eslint-disable-line no-confusing-arrow promise.catch(error => canceled ? cancel({ isCanceled: true }) : cancel(error)); // eslint-disable-line no-confusing-arrow }); return { promise: wrappedPromise, cancel() { canceled = true; }, }; })(); this.loadPromise.promise .then(this.onScriptLoaded) .catch(this.onScriptError); document.body.appendChild(script); } componentDidUpdate() { if (!scriptLoading) { this.updateStripeHandler(); } } componentWillUnmount() { this._isMounted = false; if (this.loadPromise) { this.loadPromise.cancel(); } if (ReactStripeCheckout.stripeHandler && this.state.open) { ReactStripeCheckout.stripeHandler.close(); } } onScriptLoaded = () => { if (!ReactStripeCheckout.stripeHandler) { ReactStripeCheckout.stripeHandler = StripeCheckout.configure({ key: this.props.stripeKey, }); if (this.hasPendingClick) { this.showStripeDialog(); } } } onScriptError = (...args) => { this.hideLoadingDialog(); if (this.props.onScriptError) { this.props.onScriptError.apply(this, args); } } onClosed = (...args) => { if (this._isMounted) this.setState({ open: false }); if (this.props.closed) { this.props.closed.apply(this, args); } } onOpened = (...args) => { this.setState({ open: true }); if (this.props.opened) { this.props.opened.apply(this, args); } } getConfig = () => [ 'token', 'image', 'name', 'description', 'amount', 'locale', 'currency', 'panelLabel', 'zipCode', 'shippingAddress', 'billingAddress', 'email', 'allowRememberMe', 'bitcoin', 'alipay', 'alipayReusable', ].reduce((config, key) => Object.assign({}, config, this.props.hasOwnProperty(key) && { [key]: this.props[key], }), { opened: this.onOpened, closed: this.onClosed, }); updateStripeHandler() { if (!ReactStripeCheckout.stripeHandler || this.props.reconfigureOnUpdate) { ReactStripeCheckout.stripeHandler = StripeCheckout.configure({ key: this.props.stripeKey, }); } } showLoadingDialog(...args) { if (this.props.showLoadingDialog) { this.props.showLoadingDialog.apply(this, args); } } hideLoadingDialog(...args) { if (this.props.hideLoadingDialog) { this.props.hideLoadingDialog.apply(this, args); } } showStripeDialog() { this.hideLoadingDialog(); ReactStripeCheckout.stripeHandler.open(this.getConfig()); } onClick = () => { // eslint-disable-line react/sort-comp if (this.props.disabled) { return; } if (scriptDidError) { try { throw new Error('Tried to call onClick, but StripeCheckout failed to load'); } catch (x) {} // eslint-disable-line no-empty } else if (ReactStripeCheckout.stripeHandler) { this.showStripeDialog(); } else { this.showLoadingDialog(); this.hasPendingClick = true; } } handleOnMouseDown = () => { this.setState({ buttonActive: true, }); } handleOnMouseUp = () => { this.setState({ buttonActive: false, }); } renderDefaultStripeButton() { return ( <button {...{ [this.props.triggerEvent]: this.onClick, }} className={this.props.className} onMouseDown={this.handleOnMouseDown} onFocus={this.handleOnMouseDown} onMouseUp={this.handleOnMouseUp} onMouseOut={this.handleOnMouseUp} onBlur={this.handleOnMouseUp} style={Object.assign({}, { overflow: 'hidden', display: 'inline-block', background: 'linear-gradient(#28a0e5,#015e94)', border: 0, padding: 1, textDecoration: 'none', borderRadius: 5, boxShadow: '0 1px 0 rgba(0,0,0,0.2)', cursor: 'pointer', visibility: 'visible', userSelect: 'none', }, this.state.buttonActive && { background: '#005d93', }, this.props.style)} > <span style={Object.assign({}, { backgroundImage: 'linear-gradient(#7dc5ee,#008cdd 85%,#30a2e4)', fontFamily: '"Helvetica Neue",Helvetica,Arial,sans-serif', fontSize: 14, position: 'relative', padding: '0 12px', display: 'block', height: 30, lineHeight: '30px', color: '#fff', fontWeight: 'bold', boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.25)', textShadow: '0 -1px 0 rgba(0,0,0,0.25)', borderRadius: 4, }, this.state.buttonActive && { color: '#eee', boxShadow: 'inset 0 1px 0 rgba(0,0,0,0.1)', backgroundImage: 'linear-gradient(#008cdd,#008cdd 85%,#239adf)', }, this.props.textStyle)} > {this.props.label} </span> </button> ); } renderDisabledButton() { return ( <button disabled style={{ background: 'rgba(0,0,0,0.2)', overflow: 'hidden', display: 'inline-block', border: 0, padding: 1, textDecoration: 'none', borderRadius: 5, userSelect: 'none', }} > <span style={{ boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.25)', fontFamily: '"Helvetica Neue",Helvetica,Arial,sans-serif', fontSize: 14, position: 'relative', padding: '0 12px', display: 'block', height: 30, lineHeight: '30px', borderRadius: 4, color: '#999', background: '#f8f9fa', textShadow: '0 1px 0 rgba(255,255,255,0.5)', }} > {this.props.label} </span> </button> ); } render() { if (this.props.desktopShowModal === true && !this.state.open) { this.onClick(); } else if (this.props.desktopShowModal === false && this.state.open) { ReactStripeCheckout.stripeHandler.close(); } const { ComponentClass } = this.props; if (this.props.children) { return ( <ComponentClass {...{ [this.props.triggerEvent]: this.onClick, }} children={this.props.children} /> ); } return this.props.disabled ? this.renderDisabledButton() : this.renderDefaultStripeButton(); } }