@@ -15,11 +15,36 @@ import {
15
15
DEFAULT_CONTAINER_ID ,
16
16
DEFAULT_ONLOAD_NAME ,
17
17
DEFAULT_SCRIPT_ID ,
18
- checkElementExistence ,
19
18
getTurnstileSizeOpts ,
20
19
injectTurnstileScript
21
20
} from './utils'
22
21
22
+ let turnstileState : 'unloaded' | 'loading' | 'ready' = 'unloaded'
23
+
24
+ let turnstileLoad : {
25
+ resolve : ( value ?: unknown ) => void
26
+ reject : ( reason ?: unknown ) => void
27
+ }
28
+
29
+ const turnstileLoadPromise = new Promise ( ( resolve , reject ) => {
30
+ turnstileLoad = { resolve, reject }
31
+ if ( turnstileState === 'ready' ) resolve ( undefined )
32
+ } )
33
+
34
+ const ensureTurnstile = ( onLoadCallbackName = DEFAULT_ONLOAD_NAME ) => {
35
+ if ( turnstileState === 'unloaded' ) {
36
+ turnstileState = 'loading'
37
+ // @ts -expect-error implicit any
38
+ window [ onLoadCallbackName ] = ( ) => {
39
+ turnstileLoad . resolve ( )
40
+ turnstileState = 'ready'
41
+ // @ts -expect-error implicit any
42
+ delete window [ onLoadCallbackName ]
43
+ }
44
+ }
45
+ return turnstileLoadPromise
46
+ }
47
+
23
48
export const Turnstile = forwardRef < TurnstileInstance | undefined , TurnstileProps > ( ( props , ref ) => {
24
49
const {
25
50
scriptOptions,
@@ -49,19 +74,16 @@ export const Turnstile = forwardRef<TurnstileInstance | undefined, TurnstileProp
49
74
: CONTAINER_STYLE_SET [ widgetSize ]
50
75
)
51
76
const containerRef = useRef < HTMLElement | null > ( null )
52
- const firstRendered = useRef ( false )
53
77
const [ turnstileLoaded , setTurnstileLoaded ] = useState ( false )
54
78
const widgetId = useRef < string | undefined | null > ( )
55
79
const widgetSolved = useRef ( false )
56
80
const containerId = id || DEFAULT_CONTAINER_ID
57
- const scriptId = injectScript
58
- ? scriptOptions ?. id || `${ DEFAULT_SCRIPT_ID } __${ containerId } `
59
- : scriptOptions ?. id || DEFAULT_SCRIPT_ID
81
+
82
+ const scriptId = scriptOptions ?. id || DEFAULT_SCRIPT_ID
60
83
const scriptLoaded = useObserveScript ( scriptId )
84
+ const onLoadCallbackName = scriptOptions ?. onLoadCallbackName || DEFAULT_ONLOAD_NAME
61
85
62
- const onLoadCallbackName = scriptOptions ?. onLoadCallbackName
63
- ? `${ scriptOptions . onLoadCallbackName } __${ containerId } `
64
- : `${ DEFAULT_ONLOAD_NAME } __${ containerId } `
86
+ const appearance = options . appearance || 'always'
65
87
66
88
const renderConfig = useMemo (
67
89
( ) : RenderOptions => ( {
@@ -90,24 +112,73 @@ export const Turnstile = forwardRef<TurnstileInstance | undefined, TurnstileProp
90
112
appearance : options . appearance || 'always'
91
113
} ) ,
92
114
[
115
+ options . action ,
116
+ options . appearance ,
117
+ options . cData ,
118
+ options . execution ,
119
+ options . language ,
120
+ options . refreshExpired ,
121
+ options . responseField ,
122
+ options . responseFieldName ,
123
+ options . retry ,
124
+ options . retryInterval ,
125
+ options . tabIndex ,
126
+ options . theme ,
93
127
siteKey ,
94
- options ,
95
- onSuccess ,
96
- onError ,
97
- onExpire ,
98
- widgetSize ,
99
- onBeforeInteractive ,
100
- onAfterInteractive ,
101
- onUnsupported
128
+ widgetSize
102
129
]
103
130
)
104
131
105
- const renderConfigStringified = useMemo ( ( ) => JSON . stringify ( renderConfig ) , [ renderConfig ] )
106
-
107
132
const checkIfTurnstileLoaded = useCallback ( ( ) => {
108
133
return typeof window !== 'undefined' && ! ! window . turnstile
109
134
} , [ ] )
110
135
136
+ useEffect (
137
+ function inject ( ) {
138
+ if ( injectScript && ! turnstileLoaded ) {
139
+ injectTurnstileScript ( {
140
+ onLoadCallbackName,
141
+ scriptOptions : {
142
+ ...scriptOptions ,
143
+ id : scriptId
144
+ }
145
+ } )
146
+ }
147
+ } ,
148
+ [ injectScript , turnstileLoaded , scriptOptions , scriptId ]
149
+ )
150
+
151
+ useEffect ( function waitForTurnstile ( ) {
152
+ if ( turnstileState !== 'ready' ) {
153
+ ensureTurnstile ( onLoadCallbackName )
154
+ . then ( ( ) => setTurnstileLoaded ( true ) )
155
+ . catch ( console . error )
156
+ }
157
+ } , [ ] )
158
+
159
+ useEffect (
160
+ function renderWidget ( ) {
161
+ if ( ! containerRef . current ) return
162
+ if ( ! turnstileLoaded ) return
163
+ let cancelled = false
164
+
165
+ const render = async ( ) => {
166
+ if ( cancelled || ! containerRef . current ) return
167
+ const id = window . turnstile ! . render ( containerRef . current , renderConfig )
168
+ widgetId . current = id
169
+ if ( widgetId . current ) onWidgetLoad ?.( widgetId . current )
170
+ }
171
+
172
+ render ( )
173
+
174
+ return ( ) => {
175
+ cancelled = true
176
+ if ( widgetId . current ) window . turnstile ! . remove ( widgetId . current )
177
+ }
178
+ } ,
179
+ [ containerId , turnstileLoaded , renderConfig ]
180
+ )
181
+
111
182
useImperativeHandle (
112
183
ref ,
113
184
( ) => {
@@ -202,6 +273,7 @@ export const Turnstile = forwardRef<TurnstileInstance | undefined, TurnstileProp
202
273
203
274
const id = turnstile . render ( containerRef . current , renderConfig )
204
275
widgetId . current = id
276
+ if ( widgetId . current ) onWidgetLoad ?.( widgetId . current )
205
277
206
278
if ( options . execution !== 'execute' ) {
207
279
setContainerStyle ( CONTAINER_STYLE_SET [ widgetSize ] )
@@ -240,101 +312,41 @@ export const Turnstile = forwardRef<TurnstileInstance | undefined, TurnstileProp
240
312
}
241
313
}
242
314
} ,
243
- // eslint-disable-next-line react-hooks/exhaustive-deps
244
315
[
245
316
widgetId ,
246
317
options . execution ,
247
318
widgetSize ,
248
319
renderConfig ,
249
320
containerRef ,
250
321
checkIfTurnstileLoaded ,
251
- turnstileLoaded
322
+ turnstileLoaded ,
323
+ onWidgetLoad
252
324
]
253
325
)
254
326
255
- useEffect ( ( ) => {
256
- // @ts -expect-error implicit any
257
- window [ onLoadCallbackName ] = ( ) => setTurnstileLoaded ( true )
258
-
259
- return ( ) => {
260
- // @ts -expect-error implicit any
261
- delete window [ onLoadCallbackName ]
262
- }
263
- } , [ onLoadCallbackName ] )
264
-
265
- useEffect ( ( ) => {
266
- if ( injectScript && ! turnstileLoaded ) {
267
- injectTurnstileScript ( {
268
- onLoadCallbackName,
269
- scriptOptions : {
270
- ...scriptOptions ,
271
- id : scriptId
272
- }
273
- } )
274
- }
275
- } , [ injectScript , turnstileLoaded , onLoadCallbackName , scriptOptions , scriptId ] )
276
-
277
327
/* Set the turnstile as loaded, in case the onload callback never runs. (e.g., when manually injecting the script without specifying the `onload` param) */
278
328
useEffect ( ( ) => {
279
329
if ( scriptLoaded && ! turnstileLoaded && window . turnstile ) {
280
330
setTurnstileLoaded ( true )
281
331
}
282
332
} , [ turnstileLoaded , scriptLoaded ] )
283
333
284
- useEffect ( ( ) => {
285
- if ( ! siteKey ) {
286
- console . warn ( 'sitekey was not provided' )
287
- return
288
- }
289
-
290
- if ( ! scriptLoaded || ! containerRef . current || ! turnstileLoaded || firstRendered . current ) {
291
- return
292
- }
293
-
294
- const id = window . turnstile ! . render ( containerRef . current , renderConfig )
295
- widgetId . current = id
296
- firstRendered . current = true
297
- } , [ scriptLoaded , siteKey , renderConfig , firstRendered , turnstileLoaded ] )
298
-
299
- // re-render widget when renderConfig changes
300
- useEffect ( ( ) => {
301
- if ( ! window . turnstile ) return
302
-
303
- if ( containerRef . current && widgetId . current ) {
304
- if ( checkElementExistence ( widgetId . current ) ) {
305
- window . turnstile . remove ( widgetId . current )
306
- }
307
- const newWidgetId = window . turnstile . render ( containerRef . current , renderConfig )
308
- widgetId . current = newWidgetId
309
- firstRendered . current = true
310
- }
311
- // eslint-disable-next-line react-hooks/exhaustive-deps
312
- } , [ renderConfigStringified , siteKey ] )
313
-
314
- useEffect ( ( ) => {
315
- if ( ! window . turnstile ) return
316
- if ( ! widgetId . current ) return
317
- if ( ! checkElementExistence ( widgetId . current ) ) return
318
-
319
- onWidgetLoad ?.( widgetId . current )
320
- } , [ widgetId , onWidgetLoad ] )
321
-
334
+ // Update style
322
335
useEffect ( ( ) => {
323
336
setContainerStyle (
324
337
options . execution === 'execute'
325
338
? CONTAINER_STYLE_SET . invisible
326
- : renderConfig . appearance === 'interaction-only'
339
+ : appearance === 'interaction-only'
327
340
? CONTAINER_STYLE_SET . interactionOnly
328
341
: CONTAINER_STYLE_SET [ widgetSize ]
329
342
)
330
- } , [ options . execution , widgetSize , renderConfig . appearance ] )
343
+ } , [ options . execution , widgetSize , appearance ] )
331
344
332
345
// onLoadScript callback
333
346
useEffect ( ( ) => {
334
347
if ( ! scriptLoaded || typeof onLoadScript !== 'function' ) return
335
-
336
348
onLoadScript ( )
337
- } , [ scriptLoaded , onLoadScript ] )
349
+ } , [ scriptLoaded ] )
338
350
339
351
return (
340
352
< Container
0 commit comments