This repository has been archived by the owner on Mar 27, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 319
/
Provider.js
209 lines (186 loc) · 6.11 KB
/
Provider.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
// @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);
}
}