react-stripe-elements
Version:
React components for Stripe.js and Stripe Elements
210 lines (186 loc) • 6.25 kB
JavaScript
// @flow
import React from 'react';
import PropTypes from 'prop-types';
type Props = {
apiKey?: string,
stripe?: mixed,
children?: any,
};
type Meta =
| {tag: 'sync', stripe: StripeShape}
| {tag: 'async', stripe: StripeShape | null};
type StripeLoadListener = (StripeShape) => void;
// TODO(jez) 'sync' and 'async' are bad tag names.
// TODO(jez) What if redux also uses this.context.tag?
export type SyncStripeContext = {
tag: 'sync',
stripe: StripeShape,
};
export type AsyncStripeContext = {
tag: 'async',
addStripeLoadListener: (StripeLoadListener) => void,
};
export type ProviderContext = SyncStripeContext | AsyncStripeContext;
export const providerContextTypes = {
tag: PropTypes.string.isRequired,
stripe: PropTypes.object,
addStripeLoadListener: PropTypes.func,
};
const getOrCreateStripe = (apiKey: string, options: mixed): StripeShape => {
/**
* Note that this is not meant to be a generic memoization solution.
* This is specifically a solution for `StripeProvider`s being initialized
* and destroyed regularly (with the same set of props) when users only
* use `StripeProvider` for the subtree that contains their checkout form.
*/
window.Stripe.__cachedInstances = window.Stripe.__cachedInstances || {};
const cacheKey = `key=${apiKey} options=${JSON.stringify(options)}`;
const stripe =
window.Stripe.__cachedInstances[cacheKey] || window.Stripe(apiKey, options);
window.Stripe.__cachedInstances[cacheKey] = stripe;
return stripe;
};
const ensureStripeShape = (stripe: mixed): StripeShape => {
if (
stripe &&
stripe.elements &&
stripe.createSource &&
stripe.createToken &&
stripe.createPaymentMethod &&
stripe.handleCardPayment
) {
return ((stripe: any): StripeShape);
} else {
throw new Error(
"Please pass a valid Stripe object to StripeProvider. You can obtain a Stripe object by calling 'Stripe(...)' with your publishable key."
);
}
};
export default class Provider extends React.Component<Props> {
// Even though we're using flow, also use PropTypes so we can take advantage of developer warnings.
static propTypes = {
apiKey: PropTypes.string,
// PropTypes.object is the only way we can accept a Stripe instance
// eslint-disable-next-line react/forbid-prop-types
stripe: PropTypes.object,
children: PropTypes.node,
};
// on the other hand: childContextTypes is *required* to use context.
static childContextTypes = providerContextTypes;
static defaultProps = {
apiKey: undefined,
stripe: undefined,
children: null,
};
constructor(props: Props) {
super(props);
if (this.props.apiKey && this.props.stripe) {
throw new Error(
"Please pass either 'apiKey' or 'stripe' to StripeProvider, not both."
);
} else if (this.props.apiKey) {
if (!window.Stripe) {
throw new Error(
"Please load Stripe.js (https://js.stripe.com/v3/) on this page to use react-stripe-elements. If Stripe.js isn't available yet (it's loading asynchronously, or you're using server-side rendering), see https://github.com/stripe/react-stripe-elements#advanced-integrations"
);
} else {
const {apiKey, children, ...options} = this.props;
const stripe = getOrCreateStripe(apiKey, options);
this._meta = {tag: 'sync', stripe};
this._register();
}
} else if (this.props.stripe) {
// If we already have a stripe instance (in the constructor), we can behave synchronously.
const stripe = ensureStripeShape(this.props.stripe);
this._meta = {tag: 'sync', stripe};
this._register();
} else if (this.props.stripe === null) {
this._meta = {
tag: 'async',
stripe: null,
};
} else {
throw new Error(
"Please pass either 'apiKey' or 'stripe' to StripeProvider. If you're using 'stripe' but don't have a Stripe instance yet, pass 'null' explicitly."
);
}
this._didWarn = false;
this._didWakeUpListeners = false;
this._listeners = [];
}
getChildContext(): ProviderContext {
// getChildContext is run after the constructor, so we WILL have access to
// the initial state.
//
// However, context doesn't update in respnse to state changes like you
// might expect: context is pulled by the child, not pushed by the parent.
if (this._meta.tag === 'sync') {
return {
tag: 'sync',
stripe: this._meta.stripe,
};
} else {
return {
tag: 'async',
addStripeLoadListener: (fn: StripeLoadListener) => {
if (this._meta.stripe) {
fn(this._meta.stripe);
} else {
this._listeners.push(fn);
}
},
};
}
}
componentDidUpdate(prevProps: Props) {
const apiKeyChanged =
this.props.apiKey &&
prevProps.apiKey &&
this.props.apiKey !== prevProps.apiKey;
const stripeInstanceChanged =
this.props.stripe &&
prevProps.stripe &&
this.props.stripe !== prevProps.stripe;
if (
!this._didWarn &&
(apiKeyChanged || stripeInstanceChanged) &&
window.console &&
window.console.error
) {
this._didWarn = true;
// eslint-disable-next-line no-console
console.error(
'StripeProvider does not support changing the apiKey parameter.'
);
return;
}
if (!this._didWakeUpListeners && this.props.stripe) {
// Wake up the listeners if we've finally been given a StripeShape
this._didWakeUpListeners = true;
const stripe = ensureStripeShape(this.props.stripe);
this._meta.stripe = stripe;
this._register();
this._listeners.forEach((fn) => {
fn(stripe);
});
}
}
_register() {
const {stripe} = this._meta;
if (!stripe || !stripe._registerWrapper) {
return;
}
stripe._registerWrapper({
name: 'react-stripe-elements',
version: process.env.npm_package_version || null,
});
}
props: Props;
_didWarn: boolean;
_didWakeUpListeners: boolean;
_listeners: Array<StripeLoadListener>;
_meta: Meta;
render() {
return React.Children.only(this.props.children);
}
}