Turnstile improvement

This commit is contained in:
2026-09-25 12:50:15 +02:00
parent b82ef82f50
commit 6cb4168f44
17 changed files with 268 additions and 10 deletions
+3 -3
View File
@@ -17,8 +17,8 @@ export {
useConsentContext,
} from '../modules/consent/client.js'
export type { CookieBannerClassNames } from '../modules/consent/client.js'
export { Turnstile } from '../modules/turnstile/client.js'
export type { TurnstileProps } from '../modules/turnstile/client.js'
export { resolveFormMessage } from '../modules/notifications/resolveFormMessage.js'
export type { FormNotificationTexts } from '../modules/notifications/types.js'
export { Turnstile } from '../modules/turnstile/client.js'
export { TurnstileProvider, useTurnstile } from '../modules/turnstile/client.js'
export type { TurnstileProps } from '../modules/turnstile/client.js'
@@ -0,0 +1,78 @@
'use client'
import { createContext, useCallback, useContext, useState } from 'react'
import { Turnstile } from './Turnstile.js'
type TurnstileContextValue = {
/** Public site key from the provider (read server-side, passed once). */
siteKey: string | null
/** Current token (null until solved / after expiry). */
token: string | null
setToken: (t: string | null) => void
}
const TurnstileContext = createContext<TurnstileContextValue | null>(null)
/**
* Provides the Turnstile site key once for the whole app, like ConsentProvider
* for cookies. The project reads the key server-side and passes it here in the
* layout; forms then use <TurnstileWidget /> + useTurnstile() with NO per-form
* key wiring.
*
* // layout.tsx (server) → read key, pass to provider
* import { TurnstileProvider } from '@intecion/ipal-kit/client'
* const siteKey = await getTurnstileSiteKey() // your server helper
* <TurnstileProvider siteKey={siteKey}>{children}</TurnstileProvider>
*
* When siteKey is null (Turnstile not configured), widgets render nothing and
* token stays null — forms should treat "no Turnstile" as allowed in dev.
*/
export function TurnstileProvider({
siteKey,
children,
}: {
siteKey: string | null
children: React.ReactNode
}) {
const [token, setToken] = useState<string | null>(null)
return (
<TurnstileContext.Provider value={{ siteKey, token, setToken }}>
{children}
</TurnstileContext.Provider>
)
}
/**
* Hook giving a form the Turnstile token + a widget bound to the provider's key.
* No per-form siteKey plumbing — the provider supplies it.
*
* const { token, TurnstileWidget, reset } = useTurnstile()
* // in JSX: <TurnstileWidget />
* // at submit: submitForm({ ..., turnstileToken: token })
* // after submit: reset() // clear for the next submission
*/
export function useTurnstile(): {
token: string | null
TurnstileWidget: (props?: { theme?: 'light' | 'dark' | 'auto' }) => React.ReactNode
reset: () => void
/** True when Turnstile is configured (site key present). */
enabled: boolean
} {
const ctx = useContext(TurnstileContext)
if (!ctx) {
throw new Error('useTurnstile must be used within <TurnstileProvider>')
}
const { siteKey, token, setToken } = ctx
const reset = useCallback(() => setToken(null), [setToken])
const TurnstileWidget = useCallback(
(props?: { theme?: 'light' | 'dark' | 'auto' }) => {
if (!siteKey) return null
return <Turnstile siteKey={siteKey} onToken={setToken} theme={props?.theme} />
},
[siteKey, setToken],
)
return { token, TurnstileWidget, reset, enabled: Boolean(siteKey) }
}
+1
View File
@@ -3,3 +3,4 @@
// the server-only verify never leaks into a browser bundle.
export { Turnstile } from './Turnstile.js'
export type { TurnstileProps } from './Turnstile.js'
export { TurnstileProvider, useTurnstile } from './TurnstileProvider.js'
+1
View File
@@ -1,3 +1,4 @@
export { TurnstileProvider, useTurnstile } from './TurnstileProvider.js'
// Server-only exports. verify.ts imports 'server-only', so this must never be
// imported from a client component — use ./client for the widget instead.
export { verifyTurnstile } from './verify.js'