Skip to content

Commit 24f9e92

Browse files
authored
Merge pull request #33 from marsidev/fix/script-injection
2 parents c209682 + 5a93a95 commit 24f9e92

File tree

12 files changed

+63
-40
lines changed

12 files changed

+63
-40
lines changed

demos/nextjs/src/app/basic/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
'use client'
2+
13
import React from 'react'
24
import DemoWidget from '~/components/demo-widget'
35

demos/nextjs/src/app/manual-script-injection-with-custom-script-props/page.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
'use client'
2+
13
import Script from 'next/script'
24
import { SCRIPT_URL } from '@marsidev/react-turnstile'
35
import React from 'react'
@@ -6,7 +8,8 @@ import DemoWidget from '~/components/demo-widget'
68
export default function Page() {
79
return (
810
<React.Fragment>
9-
<Script id="turnstile-script" src={SCRIPT_URL} strategy="afterInteractive" />
11+
{/* We add a custom query param to the script URL to force a re-download of the script, since the manual script injection is also used in other demos. This is not needed if the script ID is the same. */}
12+
<Script id="turnstile-script" src={`${SCRIPT_URL}?v=2`} strategy="afterInteractive" />
1013

1114
<h1>Manual script injection with custom script props</h1>
1215

demos/nextjs/src/app/manual-script-injection/page.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,14 @@
11
'use client'
22

33
import Script from 'next/script'
4-
import { DEFAULT_ONLOAD_NAME, DEFAULT_SCRIPT_ID, SCRIPT_URL } from '@marsidev/react-turnstile'
4+
import { DEFAULT_SCRIPT_ID, SCRIPT_URL } from '@marsidev/react-turnstile'
55
import React from 'react'
66
import DemoWidget from '~/components/demo-widget'
77

88
export default function Page() {
99
return (
1010
<React.Fragment>
11-
<Script
12-
id={DEFAULT_SCRIPT_ID}
13-
src={`${SCRIPT_URL}?onload=${DEFAULT_ONLOAD_NAME}`}
14-
strategy="afterInteractive"
15-
/>
11+
<Script id={DEFAULT_SCRIPT_ID} src={SCRIPT_URL} strategy="afterInteractive" />
1612

1713
<h1>Manual script injection</h1>
1814
<DemoWidget injectScript={false} />

demos/nextjs/src/app/multiple-widgets/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
'use client'
2+
13
import React from 'react'
24
import DemoWidget from '~/components/demo-widget'
35

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
"eslint --cache --fix",
4343
"prettier --write --cache --ignore-unknown"
4444
],
45-
"**/*.{json}": [
45+
"**/*.json": [
4646
"prettier --write --cache --ignore-unknown"
4747
]
4848
},

packages/lib/src/lib.tsx

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
DEFAULT_CONTAINER_ID,
88
DEFAULT_ONLOAD_NAME,
99
DEFAULT_SCRIPT_ID,
10+
checkElementExistence,
1011
getTurnstileSizeOpts,
1112
injectTurnstileScript
1213
} from './utils'
@@ -41,13 +42,15 @@ export const Turnstile = forwardRef<TurnstileInstance | undefined, TurnstileProp
4142
const firstRendered = useRef(false)
4243
const [widgetId, setWidgetId] = useState<string | undefined | null>()
4344
const [turnstileLoaded, setTurnstileLoaded] = useState(false)
44-
const scriptId = scriptOptions?.id || DEFAULT_SCRIPT_ID
45-
const scriptLoaded = useObserveScript(scriptId)
4645
const containerId = id ?? DEFAULT_CONTAINER_ID
46+
const scriptId = injectScript
47+
? scriptOptions?.id || `${DEFAULT_SCRIPT_ID}__${containerId}`
48+
: scriptOptions?.id || DEFAULT_SCRIPT_ID
49+
const scriptLoaded = useObserveScript(scriptId)
4750

48-
const onLoadCallbackName = `${
49-
scriptOptions?.onLoadCallbackName || DEFAULT_ONLOAD_NAME
50-
}#${containerId}`
51+
const onLoadCallbackName = scriptOptions?.onLoadCallbackName
52+
? `${scriptOptions.onLoadCallbackName}__${containerId}`
53+
: `${DEFAULT_ONLOAD_NAME}__${containerId}`
5154

5255
const renderConfig = useMemo(
5356
(): RenderOptions => ({
@@ -179,18 +182,22 @@ export const Turnstile = forwardRef<TurnstileInstance | undefined, TurnstileProp
179182

180183
useEffect(() => {
181184
if (injectScript && !turnstileLoaded) {
182-
injectTurnstileScript({ onLoadCallbackName, scriptOptions })
185+
injectTurnstileScript({
186+
onLoadCallbackName,
187+
scriptOptions: {
188+
...scriptOptions,
189+
id: scriptId
190+
}
191+
})
183192
}
184-
}, [injectScript, turnstileLoaded, onLoadCallbackName, scriptOptions])
193+
}, [injectScript, turnstileLoaded, onLoadCallbackName, scriptOptions, scriptId])
185194

186-
/* if the script is injected by the user, we need to wait for turnstile to be loaded
187-
and set turnstileLoaded to true. Different from the case when handle the injection,
188-
where we set turnstileLoaded in the script.onload callback */
195+
/* Set the turnstile as loaded, in case the onload callback never runs. (e.g., when manually injecting the script without specifying the `onload` param) */
189196
useEffect(() => {
190-
if (!injectScript && scriptLoaded && !turnstileLoaded && window.turnstile) {
197+
if (scriptLoaded && !turnstileLoaded && window.turnstile) {
191198
setTurnstileLoaded(true)
192199
}
193-
}, [injectScript, turnstileLoaded, scriptLoaded])
200+
}, [turnstileLoaded, scriptLoaded])
194201

195202
useEffect(() => {
196203
if (!siteKey) {
@@ -209,20 +216,26 @@ export const Turnstile = forwardRef<TurnstileInstance | undefined, TurnstileProp
209216

210217
// re-render widget when renderConfig changes
211218
useEffect(() => {
219+
if (!window.turnstile) return
220+
212221
if (containerRef.current && widgetId) {
213-
window.turnstile!.remove(widgetId)
214-
const newWidgetId = window.turnstile!.render(containerRef.current, renderConfig)
222+
if (checkElementExistence(widgetId)) {
223+
window.turnstile.remove(widgetId)
224+
}
225+
const newWidgetId = window.turnstile.render(containerRef.current, renderConfig)
215226
setWidgetId(newWidgetId)
216227
firstRendered.current = true
217228
}
218229
// eslint-disable-next-line react-hooks/exhaustive-deps
219230
}, [renderConfigStringified, siteKey])
220231

221232
useEffect(() => {
233+
if (!window.turnstile) return
234+
if (!widgetId) return
235+
if (!checkElementExistence(widgetId)) return
236+
222237
return () => {
223-
if (widgetId && window.turnstile) {
224-
window.turnstile!.remove(widgetId)
225-
}
238+
window.turnstile!.remove(widgetId)
226239
}
227240
}, [widgetId])
228241

packages/lib/src/use-observe-script.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import { useEffect, useState } from 'react'
2-
import { DEFAULT_SCRIPT_ID, isScriptInjected } from './utils'
2+
import { DEFAULT_SCRIPT_ID, checkElementExistence } from './utils'
33

44
export default function useObserveScript(scriptId = DEFAULT_SCRIPT_ID) {
55
const [scriptLoaded, setScriptLoaded] = useState(false)
66

77
useEffect(() => {
88
const checkScriptExists = () => {
9-
const script = isScriptInjected(scriptId)
10-
if (script) {
9+
if (checkElementExistence(scriptId)) {
1110
setScriptLoaded(true)
1211
}
1312
}

packages/lib/src/utils.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@ import { ContainerSizeSet, InjectTurnstileScriptParams, RenderOptions } from './
22

33
export const SCRIPT_URL = 'https://challenges.cloudflare.com/turnstile/v0/api.js'
44
export const DEFAULT_SCRIPT_ID = 'cf-turnstile-script'
5-
export const DEFAULT_ONLOAD_NAME = 'onloadTurnstileCallback'
65
export const DEFAULT_CONTAINER_ID = 'cf-turnstile'
6+
export const DEFAULT_ONLOAD_NAME = 'onloadTurnstileCallback'
77

88
/**
9-
* Function to check if script has already been injected
9+
* Function to check if an element with the given id exists in the document.
1010
*
11-
* @param scriptId
11+
* @param id Id of the element to check.
1212
* @returns
1313
*/
14-
export const isScriptInjected = (scriptId: string) => !!document.querySelector(`#${scriptId}`)
14+
export const checkElementExistence = (id: string) => !!document.getElementById(id)
1515

1616
/**
1717
* Function to inject the cloudflare turnstile script
@@ -26,11 +26,20 @@ export const injectTurnstileScript = ({
2626
}: InjectTurnstileScriptParams) => {
2727
const scriptId = id || DEFAULT_SCRIPT_ID
2828

29+
if (checkElementExistence(scriptId)) {
30+
return
31+
}
32+
2933
const script = document.createElement('script')
3034
script.id = scriptId
3135

3236
script.src = `${SCRIPT_URL}?onload=${onLoadCallbackName}&render=${render}`
3337

38+
// Prevent duplicate script injection with the same src
39+
if (document.querySelector(`script[src="${script.src}"]`)) {
40+
return
41+
}
42+
3443
script.defer = !!defer
3544
script.async = !!async
3645

packages/lib/test/index.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ describe('Basic setup', () => {
2424
it('injects the script', async () => {
2525
const script = document.querySelector('script')
2626
expect(script).toBeTruthy()
27-
expect(script?.id).toBe(DEFAULT_SCRIPT_ID)
27+
expect(script?.id).toBe(`${DEFAULT_SCRIPT_ID}__${DEFAULT_CONTAINER_ID}`)
2828
expect(script?.src).toContain(SCRIPT_URL)
2929
})
3030

test/e2e/basic.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ test.afterAll(async () => {
3232
})
3333

3434
test('script injected', async () => {
35-
await expect(page.locator(`#${DEFAULT_SCRIPT_ID}`)).toHaveCount(1)
35+
await expect(page.locator(`#${DEFAULT_SCRIPT_ID}__${DEFAULT_CONTAINER_ID}`)).toHaveCount(1)
3636
})
3737

3838
test('widget container rendered', async () => {

test/e2e/browsers.test.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,11 @@ let currentBrowser: Browser
1010
const devices = [
1111
{ name: 'Chrome', config: allDevices['Desktop Chrome'] },
1212
{ name: 'Chrome (2)', config: allDevices['Desktop Chrome'], channel: 'chrome' },
13-
{ name: 'Chrome Beta', config: allDevices['Desktop Chrome'], channel: 'chrome-beta' },
1413
{ name: 'Desktop Edge', config: allDevices['Desktop Edge'] },
1514
{ name: 'Desktop Edge (2)', config: allDevices['Desktop Edge'], channel: 'msedge' },
16-
{ name: 'Desktop Edge Beta', config: allDevices['Desktop Edge'], channel: 'msedge-beta' },
1715
{ name: 'Desktop Safari', config: allDevices['Desktop Safari'] },
1816
{ name: 'Desktop Firefox', config: allDevices['Desktop Firefox'] },
1917
{ name: 'Galaxy S9+', config: allDevices['Galaxy S9+'] },
20-
{ name: 'Galaxy Tab S4', config: allDevices['Galaxy Tab S4'] },
21-
{ name: 'iPad Pro 11', config: allDevices['iPad Pro 11'] },
2218
{ name: 'iPhone 13 Pro Max', config: allDevices['iPhone 13 Pro Max'] }
2319
]
2420

@@ -63,7 +59,9 @@ describe('Browsers', async () => {
6359
const page = await context.newPage()
6460
await page.goto('/')
6561

66-
await expect(page.locator(`#${DEFAULT_SCRIPT_ID}`)).toHaveCount(1)
62+
await expect(
63+
page.locator(`#${DEFAULT_SCRIPT_ID}__${DEFAULT_CONTAINER_ID}`)
64+
).toHaveCount(1)
6765
await expect(page.locator(`#${DEFAULT_CONTAINER_ID}`)).toHaveCount(1)
6866
await ensureFrameVisible(page)
6967
const iframe = page.frameLocator('iframe[src^="https://challenges.cloudflare.com"]')

test/e2e/multiple-widgets.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ test.afterAll(async () => {
2323
})
2424

2525
test('script injected', async () => {
26-
await expect(page.locator(`#${DEFAULT_SCRIPT_ID}`)).toHaveCount(2)
26+
await expect(page.locator(`#${DEFAULT_SCRIPT_ID}__widget-1`)).toHaveCount(1)
27+
await expect(page.locator(`#${DEFAULT_SCRIPT_ID}__widget-2`)).toHaveCount(1)
2728
})
2829

2930
test('widget containers rendered', async () => {

0 commit comments

Comments
 (0)