1
1
// @flow
2
2
/* eslint-disable react/forbid-prop-types */
3
- import React , { useContext , useMemo , useState } from 'react' ;
3
+ import React , { useContext , useMemo , useState , useEffect , useRef } from 'react' ;
4
4
import PropTypes from 'prop-types' ;
5
5
6
6
import isEqual from '../utils/isEqual' ;
7
7
import usePrevious from '../utils/usePrevious' ;
8
8
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 > | } ;
54
13
55
14
// We are using types to enforce the `stripe` prop in this lib,
56
15
// but in a real integration `stripe` could be anything, so we need
57
16
// to do some sanity validation to prevent type errors.
58
- const validateStripe = ( maybeStripe : mixed ) : StripeShape | null = > {
17
+ const validateStripe = ( maybeStripe : mixed ) : null | StripeShape => {
59
18
if ( maybeStripe === null ) {
60
19
return maybeStripe ;
61
20
}
62
21
63
22
if (
64
23
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'
69
28
) {
29
+ // If the object appears to be roughly Stripe shaped,
30
+ // force cast it to the expected type.
70
31
return ( ( maybeStripe : any ) : StripeShape ) ;
71
32
}
72
33
@@ -76,44 +37,112 @@ const validateStripe = (maybeStripe: mixed): StripeShape | null => {
76
37
) ;
77
38
} ;
78
39
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 ,
80
64
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 ,
81
84
options ?: MixedObject ,
82
85
children ?: any ,
83
86
| } ;
84
87
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
+ } ) ) ;
94
96
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
+ }
97
110
}
98
111
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
+ }
104
135
}
105
136
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
+ ) ;
112
143
113
144
return (
114
- < ElementsContext . Provider value = { elements } >
115
- { children }
116
- </ ElementsContext . Provider >
145
+ < ElementsContext . Provider value = { ctx } > { children } </ ElementsContext . Provider >
117
146
) ;
118
147
} ;
119
148
@@ -130,13 +159,11 @@ export const useElementsContextWithUseCase = (useCaseMessage: string) => {
130
159
131
160
export const useElements = ( ) : ElementsShape | null => {
132
161
const { elements} = useElementsContextWithUseCase ( 'calls useElements()' ) ;
133
-
134
162
return elements ;
135
163
} ;
136
164
137
165
export const useStripe = ( ) : StripeShape | null => {
138
166
const { stripe} = useElementsContextWithUseCase ( 'calls useStripe()' ) ;
139
-
140
167
return stripe ;
141
168
} ;
142
169
0 commit comments