Skip to content

Commit 8eb0af0

Browse files
Enable passing a Promise to the stripe prop of <Elements> (#13)
* enable passing a promise to the stripe prop * rework stripe prop handling
1 parent 4cb745d commit 8eb0af0

File tree

7 files changed

+267
-149
lines changed

7 files changed

+267
-149
lines changed

examples/class-components/1-Card-Async.js

Lines changed: 22 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -69,39 +69,30 @@ const InjectedCheckoutForm = () => {
6969
);
7070
};
7171

72-
class App extends React.Component {
73-
constructor(props) {
74-
super(props);
75-
this.state = {
76-
stripe: null,
77-
};
72+
const stripePromise = new Promise((resolve) => {
73+
if (typeof window === 'undefined') {
74+
// We can also make this work with server side rendering (SSR) by
75+
// resolving to null when not in a browser environment.
76+
resolve(null);
7877
}
7978

80-
componentDidMount() {
81-
// componentDidMount is only called in browser environments,
82-
// so this will also work with server side rendering (SSR).
83-
84-
// You can inject a script tag manually like this, or you can just
85-
// use the 'async' attribute on the Stripe.js v3 script tag.
86-
const stripeJs = document.createElement('script');
87-
stripeJs.src = 'https://js.stripe.com/v3/';
88-
stripeJs.async = true;
89-
stripeJs.onload = () => {
90-
this.setState({
91-
stripe: window.Stripe('pk_test_6pRNASCoBOKtIshFeQd4XMUh'),
92-
});
93-
};
94-
document.body.appendChild(stripeJs);
95-
}
79+
// You can inject a script tag manually like this, or you can just
80+
// use the 'async' attribute on the Stripe.js v3 script tag.
81+
const stripeJs = document.createElement('script');
82+
stripeJs.src = 'https://js.stripe.com/v3/';
83+
stripeJs.async = true;
84+
stripeJs.onload = () => {
85+
resolve(window.Stripe('pk_test_6pRNASCoBOKtIshFeQd4XMUh'));
86+
};
87+
document.body.appendChild(stripeJs);
88+
});
9689

97-
render() {
98-
const {stripe} = this.state;
99-
return (
100-
<Elements stripe={stripe}>
101-
<InjectedCheckoutForm />
102-
</Elements>
103-
);
104-
}
105-
}
90+
const App = () => {
91+
return (
92+
<Elements stripe={stripePromise}>
93+
<InjectedCheckoutForm />
94+
</Elements>
95+
);
96+
};
10697

10798
export default App;

examples/hooks/1-Card-Async.js

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// @noflow
22

3-
import React, {useState, useEffect} from 'react';
3+
import React from 'react';
44
import {CardElement, Elements, useElements, useStripe} from '../../src';
55
import '../styles/common.css';
66

@@ -60,29 +60,27 @@ const MyCheckoutForm = () => {
6060
);
6161
};
6262

63-
const App = () => {
64-
const [stripe, setStripe] = useState(null);
65-
66-
useEffect(() => {
67-
if (typeof window === 'undefined') {
68-
// We can also make this work with server side rendering (SSR) by
69-
// returning early when not in a browser environment.
70-
return;
71-
}
63+
const stripePromise = new Promise((resolve) => {
64+
if (typeof window === 'undefined') {
65+
// We can also make this work with server side rendering (SSR) by
66+
// resolving to null when not in a browser environment.
67+
resolve(null);
68+
}
7269

73-
// You can inject a script tag manually like this, or you can just
74-
// use the 'async' attribute on the Stripe.js v3 script tag.
75-
const stripeJs = document.createElement('script');
76-
stripeJs.src = 'https://js.stripe.com/v3/';
77-
stripeJs.async = true;
78-
stripeJs.onload = () => {
79-
setStripe(window.Stripe('pk_test_6pRNASCoBOKtIshFeQd4XMUh'));
80-
};
81-
document.body.appendChild(stripeJs);
82-
}, []);
70+
// You can inject a script tag manually like this, or you can just
71+
// use the 'async' attribute on the Stripe.js v3 script tag.
72+
const stripeJs = document.createElement('script');
73+
stripeJs.src = 'https://js.stripe.com/v3/';
74+
stripeJs.async = true;
75+
stripeJs.onload = () => {
76+
resolve(window.Stripe('pk_test_6pRNASCoBOKtIshFeQd4XMUh'));
77+
};
78+
document.body.appendChild(stripeJs);
79+
});
8380

81+
const App = () => {
8482
return (
85-
<Elements stripe={stripe}>
83+
<Elements stripe={stripePromise}>
8684
<MyCheckoutForm />
8785
</Elements>
8886
);

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@
7272
"flow-bin": "^0.111.3",
7373
"jest": "^24.9.0",
7474
"prettier": "^1.19.1",
75-
"react": "~16.8.0",
76-
"react-dom": "~16.8.0",
75+
"react": "~16.9.0",
76+
"react-dom": "~16.9.0",
7777
"rimraf": "^2.6.2",
7878
"rollup": "^1.27.0",
7979
"rollup-plugin-babel": "^4.3.3",

src/components/Elements.js

Lines changed: 106 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,33 @@
11
// @flow
22
/* eslint-disable react/forbid-prop-types */
3-
import React, {useContext, useMemo, useState} from 'react';
3+
import React, {useContext, useMemo, useState, useEffect, useRef} from 'react';
44
import PropTypes from 'prop-types';
55

66
import isEqual from '../utils/isEqual';
77
import usePrevious from '../utils/usePrevious';
88

9-
type ElementContext =
10-
| {|
11-
tag: 'ready',
12-
elements: ElementsShape,
13-
stripe: StripeShape,
14-
|}
15-
| {|
16-
tag: 'loading',
17-
|};
18-
19-
const ElementsContext = React.createContext<?ElementContext>();
20-
21-
export const createElementsContext = (
22-
stripe: null | StripeShape,
23-
options: ?MixedObject
24-
): ElementContext => {
25-
if (stripe === null) {
26-
return {tag: 'loading'};
27-
}
28-
29-
return {
30-
tag: 'ready',
31-
stripe,
32-
elements: stripe.elements(options || {}),
33-
};
34-
};
35-
36-
export const parseElementsContext = (
37-
ctx: ?ElementContext,
38-
useCase: string
39-
): {|elements: ElementsShape | null, stripe: StripeShape | null|} => {
40-
if (!ctx) {
41-
throw new Error(
42-
`Could not find Elements context; You need to wrap the part of your app that ${useCase} in an <Elements> provider.`
43-
);
44-
}
45-
46-
if (ctx.tag === 'loading') {
47-
return {stripe: null, elements: null};
48-
}
49-
50-
const {stripe, elements} = ctx;
51-
52-
return {stripe, elements};
53-
};
9+
type ParsedStripeProp =
10+
| {|tag: 'empty'|}
11+
| {|tag: 'sync', stripe: StripeShape|}
12+
| {|tag: 'async', stripePromise: Promise<StripeShape | null>|};
5413

5514
// We are using types to enforce the `stripe` prop in this lib,
5615
// but in a real integration `stripe` could be anything, so we need
5716
// to do some sanity validation to prevent type errors.
58-
const validateStripe = (maybeStripe: mixed): StripeShape | null => {
17+
const validateStripe = (maybeStripe: mixed): null | StripeShape => {
5918
if (maybeStripe === null) {
6019
return maybeStripe;
6120
}
6221

6322
if (
6423
typeof maybeStripe === 'object' &&
65-
maybeStripe.elements &&
66-
maybeStripe.createSource &&
67-
maybeStripe.createToken &&
68-
maybeStripe.createPaymentMethod
24+
typeof maybeStripe.elements === 'function' &&
25+
typeof maybeStripe.createSource === 'function' &&
26+
typeof maybeStripe.createToken === 'function' &&
27+
typeof maybeStripe.createPaymentMethod === 'function'
6928
) {
29+
// If the object appears to be roughly Stripe shaped,
30+
// force cast it to the expected type.
7031
return ((maybeStripe: any): StripeShape);
7132
}
7233

@@ -76,44 +37,112 @@ const validateStripe = (maybeStripe: mixed): StripeShape | null => {
7637
);
7738
};
7839

79-
type Props = {|
40+
const parseStripeProp = (raw: mixed): ParsedStripeProp => {
41+
if (
42+
raw !== null &&
43+
typeof raw === 'object' &&
44+
raw.then &&
45+
typeof raw.then === 'function'
46+
) {
47+
return {
48+
tag: 'async',
49+
stripePromise: Promise.resolve(raw).then(validateStripe),
50+
};
51+
}
52+
53+
const stripe = validateStripe(raw);
54+
55+
if (stripe === null) {
56+
return {tag: 'empty'};
57+
}
58+
59+
return {tag: 'sync', stripe};
60+
};
61+
62+
type ElementContext = {|
63+
elements: ElementsShape | null,
8064
stripe: StripeShape | null,
65+
|};
66+
67+
const ElementsContext = React.createContext<?ElementContext>();
68+
69+
export const parseElementsContext = (
70+
ctx: ?ElementContext,
71+
useCase: string
72+
): ElementContext => {
73+
if (!ctx) {
74+
throw new Error(
75+
`Could not find Elements context; You need to wrap the part of your app that ${useCase} in an <Elements> provider.`
76+
);
77+
}
78+
79+
return ctx;
80+
};
81+
82+
type Props = {|
83+
stripe: Promise<StripeShape | null> | StripeShape | null,
8184
options?: MixedObject,
8285
children?: any,
8386
|};
8487

85-
export const Elements = ({stripe: rawStripe, options, children}: Props) => {
86-
// We are using types to enforce the `stripe` prop in this lib,
87-
// but in a real integration, `stripe` could be anything, so we need
88-
// to do some sanity validation to prevent type errors.
89-
const stripe = useMemo(() => validateStripe(rawStripe), [rawStripe]);
90-
91-
const [elements, setElements] = useState(() =>
92-
createElementsContext(stripe, options)
93-
);
88+
export const Elements = ({stripe: rawStripeProp, options, children}: Props) => {
89+
const final = useRef(false);
90+
const isMounted = useRef(true);
91+
const parsed = useMemo(() => parseStripeProp(rawStripeProp), [rawStripeProp]);
92+
const [ctx, setContext] = useState<ElementContext>(() => ({
93+
stripe: null,
94+
elements: null,
95+
}));
9496

95-
if (stripe !== null && elements.tag === 'loading') {
96-
setElements(createElementsContext(stripe, options));
97+
const prevStripe = usePrevious(rawStripeProp);
98+
const prevOptions = usePrevious(options);
99+
if (prevStripe !== null) {
100+
if (prevStripe !== rawStripeProp) {
101+
console.warn(
102+
'Unsupported prop change on Elements: You cannot change the `stripe` prop after setting it.'
103+
);
104+
}
105+
if (!isEqual(options, prevOptions)) {
106+
console.warn(
107+
'Unsupported prop change on Elements: You cannot change the `options` prop after setting the `stripe` prop.'
108+
);
109+
}
97110
}
98111

99-
const prevStripe = usePrevious(stripe);
100-
if (prevStripe != null && prevStripe !== stripe) {
101-
console.warn(
102-
'Unsupported prop change on Elements: You cannot change the `stripe` prop after setting it.'
103-
);
112+
if (!final.current) {
113+
if (parsed.tag === 'sync') {
114+
final.current = true;
115+
setContext({
116+
stripe: parsed.stripe,
117+
elements: parsed.stripe.elements(options),
118+
});
119+
}
120+
121+
if (parsed.tag === 'async') {
122+
final.current = true;
123+
parsed.stripePromise.then((stripe) => {
124+
if (stripe && isMounted.current) {
125+
// Only update Elements context if the component is still mounted
126+
// and stripe is not null. We allow stripe to be null to make
127+
// handling SSR easier.
128+
setContext({
129+
stripe,
130+
elements: stripe.elements(options),
131+
});
132+
}
133+
});
134+
}
104135
}
105136

106-
const prevOptions = usePrevious(options);
107-
if (prevStripe !== null && !isEqual(prevOptions, options)) {
108-
console.warn(
109-
'Unsupported prop change on Elements: You cannot change the `options` prop after setting the `stripe` prop.'
110-
);
111-
}
137+
useEffect(
138+
() => () => {
139+
isMounted.current = false;
140+
},
141+
[]
142+
);
112143

113144
return (
114-
<ElementsContext.Provider value={elements}>
115-
{children}
116-
</ElementsContext.Provider>
145+
<ElementsContext.Provider value={ctx}>{children}</ElementsContext.Provider>
117146
);
118147
};
119148

@@ -130,13 +159,11 @@ export const useElementsContextWithUseCase = (useCaseMessage: string) => {
130159

131160
export const useElements = (): ElementsShape | null => {
132161
const {elements} = useElementsContextWithUseCase('calls useElements()');
133-
134162
return elements;
135163
};
136164

137165
export const useStripe = (): StripeShape | null => {
138166
const {stripe} = useElementsContextWithUseCase('calls useStripe()');
139-
140167
return stripe;
141168
};
142169

0 commit comments

Comments
 (0)