diff --git a/dist/exports/client.d.ts b/dist/exports/client.d.ts index 7a4c12e..1e66075 100644 --- a/dist/exports/client.d.ts +++ b/dist/exports/client.d.ts @@ -10,7 +10,8 @@ export { Analytics } from '../modules/analytics/client.js'; */ export { ConsentProvider, CookieBanner, CookieButton, useConsent, 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'; diff --git a/dist/exports/client.js b/dist/exports/client.js index 40e9411..e4b2dc0 100644 --- a/dist/exports/client.js +++ b/dist/exports/client.js @@ -9,7 +9,8 @@ export { Analytics } from '../modules/analytics/client.js'; * components. Kept separate from the main entry so server bundles don't pull in * client-only code. */ export { ConsentProvider, CookieBanner, CookieButton, useConsent, useConsentContext } from '../modules/consent/client.js'; -export { Turnstile } from '../modules/turnstile/client.js'; export { resolveFormMessage } from '../modules/notifications/resolveFormMessage.js'; +export { Turnstile } from '../modules/turnstile/client.js'; +export { TurnstileProvider, useTurnstile } from '../modules/turnstile/client.js'; //# sourceMappingURL=client.js.map \ No newline at end of file diff --git a/dist/exports/client.js.map b/dist/exports/client.js.map index 2ccf330..4a8cde4 100644 --- a/dist/exports/client.js.map +++ b/dist/exports/client.js.map @@ -1 +1 @@ -{"version":3,"sources":["../../src/exports/client.ts"],"sourcesContent":["'use client'\nexport { MaskedField } from '../globals/SiteIntegrations/components/MaskedField.js'\nexport { TestEmailButton } from '../globals/SiteIntegrations/components/TestEmailButton.js'\nexport { Analytics } from '../modules/analytics/client.js'\n/**\n * Entry point: ipal-kit/client\n *\n * Client-side ('use client') exports — React hooks, providers, and UI\n * components. Kept separate from the main entry so server bundles don't pull in\n * client-only code.\n */\nexport {\n ConsentProvider,\n CookieBanner,\n CookieButton,\n useConsent,\n useConsentContext,\n} from '../modules/consent/client.js'\nexport type { CookieBannerClassNames } from '../modules/consent/client.js'\nexport { Turnstile } from '../modules/turnstile/client.js'\nexport type { TurnstileProps } from '../modules/turnstile/client.js'\n\nexport { resolveFormMessage } from '../modules/notifications/resolveFormMessage.js'\nexport type { FormNotificationTexts } from '../modules/notifications/types.js'\n"],"names":["MaskedField","TestEmailButton","Analytics","ConsentProvider","CookieBanner","CookieButton","useConsent","useConsentContext","Turnstile","resolveFormMessage"],"mappings":"AAAA;AACA,SAASA,WAAW,QAAQ,wDAAuD;AACnF,SAASC,eAAe,QAAQ,4DAA2D;AAC3F,SAASC,SAAS,QAAQ,iCAAgC;AAC1D;;;;;;CAMC,GACD,SACEC,eAAe,EACfC,YAAY,EACZC,YAAY,EACZC,UAAU,EACVC,iBAAiB,QACZ,+BAA8B;AAErC,SAASC,SAAS,QAAQ,iCAAgC;AAG1D,SAASC,kBAAkB,QAAQ,iDAAgD"} \ No newline at end of file +{"version":3,"sources":["../../src/exports/client.ts"],"sourcesContent":["'use client'\nexport { MaskedField } from '../globals/SiteIntegrations/components/MaskedField.js'\nexport { TestEmailButton } from '../globals/SiteIntegrations/components/TestEmailButton.js'\nexport { Analytics } from '../modules/analytics/client.js'\n/**\n * Entry point: ipal-kit/client\n *\n * Client-side ('use client') exports — React hooks, providers, and UI\n * components. Kept separate from the main entry so server bundles don't pull in\n * client-only code.\n */\nexport {\n ConsentProvider,\n CookieBanner,\n CookieButton,\n useConsent,\n useConsentContext,\n} from '../modules/consent/client.js'\nexport type { CookieBannerClassNames } from '../modules/consent/client.js'\nexport { resolveFormMessage } from '../modules/notifications/resolveFormMessage.js'\nexport type { FormNotificationTexts } from '../modules/notifications/types.js'\nexport { Turnstile } from '../modules/turnstile/client.js'\nexport { TurnstileProvider, useTurnstile } from '../modules/turnstile/client.js'\nexport type { TurnstileProps } from '../modules/turnstile/client.js'\n"],"names":["MaskedField","TestEmailButton","Analytics","ConsentProvider","CookieBanner","CookieButton","useConsent","useConsentContext","resolveFormMessage","Turnstile","TurnstileProvider","useTurnstile"],"mappings":"AAAA;AACA,SAASA,WAAW,QAAQ,wDAAuD;AACnF,SAASC,eAAe,QAAQ,4DAA2D;AAC3F,SAASC,SAAS,QAAQ,iCAAgC;AAC1D;;;;;;CAMC,GACD,SACEC,eAAe,EACfC,YAAY,EACZC,YAAY,EACZC,UAAU,EACVC,iBAAiB,QACZ,+BAA8B;AAErC,SAASC,kBAAkB,QAAQ,iDAAgD;AAEnF,SAASC,SAAS,QAAQ,iCAAgC;AAC1D,SAASC,iBAAiB,EAAEC,YAAY,QAAQ,iCAAgC"} \ No newline at end of file diff --git a/dist/modules/turnstile/TurnstileProvider.d.ts b/dist/modules/turnstile/TurnstileProvider.d.ts new file mode 100644 index 0000000..7b1507f --- /dev/null +++ b/dist/modules/turnstile/TurnstileProvider.d.ts @@ -0,0 +1,36 @@ +/** + * 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 + 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 + * {children} + * + * When siteKey is null (Turnstile not configured), widgets render nothing and + * token stays null — forms should treat "no Turnstile" as allowed in dev. + */ +export declare function TurnstileProvider({ siteKey, children, }: { + siteKey: string | null; + children: React.ReactNode; +}): import("react/jsx-runtime").JSX.Element; +/** + * 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: + * // at submit: submitForm({ ..., turnstileToken: token }) + * // after submit: reset() // clear for the next submission + */ +export declare 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; +}; diff --git a/dist/modules/turnstile/TurnstileProvider.js b/dist/modules/turnstile/TurnstileProvider.js new file mode 100644 index 0000000..a5760b3 --- /dev/null +++ b/dist/modules/turnstile/TurnstileProvider.js @@ -0,0 +1,66 @@ +'use client'; +import { jsx as _jsx } from "react/jsx-runtime"; +import { createContext, useCallback, useContext, useState } from 'react'; +import { Turnstile } from './Turnstile.js'; +const TurnstileContext = /*#__PURE__*/ createContext(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 + 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 + * {children} + * + * 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 }) { + const [token, setToken] = useState(null); + return /*#__PURE__*/ _jsx(TurnstileContext.Provider, { + value: { + siteKey, + token, + setToken + }, + children: children + }); +} +/** + * 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: + * // at submit: submitForm({ ..., turnstileToken: token }) + * // after submit: reset() // clear for the next submission + */ export function useTurnstile() { + const ctx = useContext(TurnstileContext); + if (!ctx) { + throw new Error('useTurnstile must be used within '); + } + const { siteKey, token, setToken } = ctx; + const reset = useCallback(()=>setToken(null), [ + setToken + ]); + const TurnstileWidget = useCallback((props)=>{ + if (!siteKey) return null; + return /*#__PURE__*/ _jsx(Turnstile, { + siteKey: siteKey, + onToken: setToken, + theme: props?.theme + }); + }, [ + siteKey, + setToken + ]); + return { + token, + TurnstileWidget, + reset, + enabled: Boolean(siteKey) + }; +} + +//# sourceMappingURL=TurnstileProvider.js.map \ No newline at end of file diff --git a/dist/modules/turnstile/TurnstileProvider.js.map b/dist/modules/turnstile/TurnstileProvider.js.map new file mode 100644 index 0000000..186483b --- /dev/null +++ b/dist/modules/turnstile/TurnstileProvider.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../../src/modules/turnstile/TurnstileProvider.tsx"],"sourcesContent":["'use client'\n\nimport { createContext, useCallback, useContext, useState } from 'react'\nimport { Turnstile } from './Turnstile.js'\n\ntype TurnstileContextValue = {\n /** Public site key from the provider (read server-side, passed once). */\n siteKey: string | null\n /** Current token (null until solved / after expiry). */\n token: string | null\n setToken: (t: string | null) => void\n}\n\nconst TurnstileContext = createContext(null)\n\n/**\n * Provides the Turnstile site key once for the whole app, like ConsentProvider\n * for cookies. The project reads the key server-side and passes it here in the\n * layout; forms then use + useTurnstile() with NO per-form\n * key wiring.\n *\n * // layout.tsx (server) → read key, pass to provider\n * import { TurnstileProvider } from '@intecion/ipal-kit/client'\n * const siteKey = await getTurnstileSiteKey() // your server helper\n * {children}\n *\n * When siteKey is null (Turnstile not configured), widgets render nothing and\n * token stays null — forms should treat \"no Turnstile\" as allowed in dev.\n */\nexport function TurnstileProvider({\n siteKey,\n children,\n}: {\n siteKey: string | null\n children: React.ReactNode\n}) {\n const [token, setToken] = useState(null)\n return (\n \n {children}\n \n )\n}\n\n/**\n * Hook giving a form the Turnstile token + a widget bound to the provider's key.\n * No per-form siteKey plumbing — the provider supplies it.\n *\n * const { token, TurnstileWidget, reset } = useTurnstile()\n * // in JSX: \n * // at submit: submitForm({ ..., turnstileToken: token })\n * // after submit: reset() // clear for the next submission\n */\nexport function useTurnstile(): {\n token: string | null\n TurnstileWidget: (props?: { theme?: 'light' | 'dark' | 'auto' }) => React.ReactNode\n reset: () => void\n /** True when Turnstile is configured (site key present). */\n enabled: boolean\n} {\n const ctx = useContext(TurnstileContext)\n if (!ctx) {\n throw new Error('useTurnstile must be used within ')\n }\n const { siteKey, token, setToken } = ctx\n\n const reset = useCallback(() => setToken(null), [setToken])\n\n const TurnstileWidget = useCallback(\n (props?: { theme?: 'light' | 'dark' | 'auto' }) => {\n if (!siteKey) return null\n return \n },\n [siteKey, setToken],\n )\n\n return { token, TurnstileWidget, reset, enabled: Boolean(siteKey) }\n}\n"],"names":["createContext","useCallback","useContext","useState","Turnstile","TurnstileContext","TurnstileProvider","siteKey","children","token","setToken","Provider","value","useTurnstile","ctx","Error","reset","TurnstileWidget","props","onToken","theme","enabled","Boolean"],"mappings":"AAAA;;AAEA,SAASA,aAAa,EAAEC,WAAW,EAAEC,UAAU,EAAEC,QAAQ,QAAQ,QAAO;AACxE,SAASC,SAAS,QAAQ,iBAAgB;AAU1C,MAAMC,iCAAmBL,cAA4C;AAErE;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASM,kBAAkB,EAChCC,OAAO,EACPC,QAAQ,EAIT;IACC,MAAM,CAACC,OAAOC,SAAS,GAAGP,SAAwB;IAClD,qBACE,KAACE,iBAAiBM,QAAQ;QAACC,OAAO;YAAEL;YAASE;YAAOC;QAAS;kBAC1DF;;AAGP;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASK;IAOd,MAAMC,MAAMZ,WAAWG;IACvB,IAAI,CAACS,KAAK;QACR,MAAM,IAAIC,MAAM;IAClB;IACA,MAAM,EAAER,OAAO,EAAEE,KAAK,EAAEC,QAAQ,EAAE,GAAGI;IAErC,MAAME,QAAQf,YAAY,IAAMS,SAAS,OAAO;QAACA;KAAS;IAE1D,MAAMO,kBAAkBhB,YACtB,CAACiB;QACC,IAAI,CAACX,SAAS,OAAO;QACrB,qBAAO,KAACH;YAAUG,SAASA;YAASY,SAAST;YAAUU,OAAOF,OAAOE;;IACvE,GACA;QAACb;QAASG;KAAS;IAGrB,OAAO;QAAED;QAAOQ;QAAiBD;QAAOK,SAASC,QAAQf;IAAS;AACpE"} \ No newline at end of file diff --git a/dist/modules/turnstile/client.d.ts b/dist/modules/turnstile/client.d.ts index 7180b99..44aa3a7 100644 --- a/dist/modules/turnstile/client.d.ts +++ b/dist/modules/turnstile/client.d.ts @@ -1,2 +1,3 @@ export { Turnstile } from './Turnstile.js'; export type { TurnstileProps } from './Turnstile.js'; +export { TurnstileProvider, useTurnstile } from './TurnstileProvider.js'; diff --git a/dist/modules/turnstile/client.js b/dist/modules/turnstile/client.js index 157876e..871f15a 100644 --- a/dist/modules/turnstile/client.js +++ b/dist/modules/turnstile/client.js @@ -2,5 +2,6 @@ // Client-only exports — the Turnstile widget. Kept separate from index.ts so // the server-only verify never leaks into a browser bundle. export { Turnstile } from './Turnstile.js'; +export { TurnstileProvider, useTurnstile } from './TurnstileProvider.js'; //# sourceMappingURL=client.js.map \ No newline at end of file diff --git a/dist/modules/turnstile/client.js.map b/dist/modules/turnstile/client.js.map index 6beab7c..79a8330 100644 --- a/dist/modules/turnstile/client.js.map +++ b/dist/modules/turnstile/client.js.map @@ -1 +1 @@ -{"version":3,"sources":["../../../src/modules/turnstile/client.ts"],"sourcesContent":["'use client'\n// Client-only exports — the Turnstile widget. Kept separate from index.ts so\n// the server-only verify never leaks into a browser bundle.\nexport { Turnstile } from './Turnstile.js'\nexport type { TurnstileProps } from './Turnstile.js'\n"],"names":["Turnstile"],"mappings":"AAAA;AACA,6EAA6E;AAC7E,4DAA4D;AAC5D,SAASA,SAAS,QAAQ,iBAAgB"} \ No newline at end of file +{"version":3,"sources":["../../../src/modules/turnstile/client.ts"],"sourcesContent":["'use client'\n// Client-only exports — the Turnstile widget. Kept separate from index.ts so\n// the server-only verify never leaks into a browser bundle.\nexport { Turnstile } from './Turnstile.js'\nexport type { TurnstileProps } from './Turnstile.js'\nexport { TurnstileProvider, useTurnstile } from './TurnstileProvider.js'\n"],"names":["Turnstile","TurnstileProvider","useTurnstile"],"mappings":"AAAA;AACA,6EAA6E;AAC7E,4DAA4D;AAC5D,SAASA,SAAS,QAAQ,iBAAgB;AAE1C,SAASC,iBAAiB,EAAEC,YAAY,QAAQ,yBAAwB"} \ No newline at end of file diff --git a/dist/modules/turnstile/index.d.ts b/dist/modules/turnstile/index.d.ts index fe1cc66..35e57cb 100644 --- a/dist/modules/turnstile/index.d.ts +++ b/dist/modules/turnstile/index.d.ts @@ -1 +1,2 @@ +export { TurnstileProvider, useTurnstile } from './TurnstileProvider.js'; export { verifyTurnstile } from './verify.js'; diff --git a/dist/modules/turnstile/index.js b/dist/modules/turnstile/index.js index 69979b6..90302d3 100644 --- a/dist/modules/turnstile/index.js +++ b/dist/modules/turnstile/index.js @@ -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'; diff --git a/dist/modules/turnstile/index.js.map b/dist/modules/turnstile/index.js.map index acf77d2..48ee066 100644 --- a/dist/modules/turnstile/index.js.map +++ b/dist/modules/turnstile/index.js.map @@ -1 +1 @@ -{"version":3,"sources":["../../../src/modules/turnstile/index.ts"],"sourcesContent":["// Server-only exports. verify.ts imports 'server-only', so this must never be\n// imported from a client component — use ./client for the widget instead.\nexport { verifyTurnstile } from './verify.js'\n"],"names":["verifyTurnstile"],"mappings":"AAAA,8EAA8E;AAC9E,0EAA0E;AAC1E,SAASA,eAAe,QAAQ,cAAa"} \ No newline at end of file +{"version":3,"sources":["../../../src/modules/turnstile/index.ts"],"sourcesContent":["export { TurnstileProvider, useTurnstile } from './TurnstileProvider.js'\n// Server-only exports. verify.ts imports 'server-only', so this must never be\n// imported from a client component — use ./client for the widget instead.\nexport { verifyTurnstile } from './verify.js'\n"],"names":["TurnstileProvider","useTurnstile","verifyTurnstile"],"mappings":"AAAA,SAASA,iBAAiB,EAAEC,YAAY,QAAQ,yBAAwB;AACxE,8EAA8E;AAC9E,0EAA0E;AAC1E,SAASC,eAAe,QAAQ,cAAa"} \ No newline at end of file diff --git a/docs/turnstile.md b/docs/turnstile.md index 7f3593e..9c62993 100644 --- a/docs/turnstile.md +++ b/docs/turnstile.md @@ -58,4 +58,73 @@ Zwraca `false` na każdy problem (brak klucza, sieć, odrzucenie) — traktuj trafi do bundla przeglądarki. > W formularzach zwykle nie wołasz `verifyTurnstile` wprost — robi to -> `submitForm` (patrz [forms.md](./forms.md)). \ No newline at end of file +> `submitForm` (patrz [forms.md](./forms.md)). + +## Uproszczone wpięcie — TurnstileProvider + useTurnstile (zalecane) + +Jak CookieBanner: siteKey raz w layoutcie, formularze biorą z kontekstu. Koniec +przekazywania siteKey do każdego formularza. + +### 1. Provider w layoutcie (raz, siteKey z serwera) + +```tsx +// app/(frontend)/[locale]/layout.tsx (server) +import { TurnstileProvider } from '@intecion/ipal-kit/client' +import { getTurnstileSiteKey } from '@/lib/payload' // Twój helper server-side + +export default async function Layout({ children }) { + const siteKey = await getTurnstileSiteKey() // z panelu (SiteIntegrations) + return ( + + + + {children} + + + + ) +} +``` + +### 2. Formularz — useTurnstile (zero plumbingu siteKey) + +```tsx +'use client' +import { useTurnstile } from '@intecion/ipal-kit/client' + +function ContactForm() { + const { token, TurnstileWidget, reset, enabled } = useTurnstile() + + async function handleSubmit(data) { + const result = await submitForm({ ...data, turnstileToken: token }) + if (result.ok) reset() // wyczyść token na następne wysłanie + } + + return ( +
+ {/* pola formularza */} + {/* widget tam, gdzie ma być */} + + + ) +} +``` + +`token` → do submitForm. `TurnstileWidget` → wstaw gdzie ma być. `reset()` → po +wysłaniu. `enabled` → false gdy brak klucza (dev bez Turnstile). + +### Dlaczego Turnstile NIE jest w pełni "wstaw i zapomnij" jak CookieBanner + +CookieBanner jest samodzielny (renderuje się, zarządza zgodą, zero interakcji). +Turnstile z natury jest CZĘŚCIĄ formularza — zwraca token, który formularz musi +wysłać przy submit i zweryfikować server-side. Nie da się go „wstawić +gdziekolwiek" — musi być w formularzu, przy jego logice wysyłki. + +Provider+hook to maksimum uproszczenia: siteKey raz (jak CookieBanner), a w +formularzu tylko `` + `token`. Reszta (weryfikacja) dzieje się +w submitForm automatycznie. + +### Stary sposób (nadal działa) + +`` bezpośrednio — jeśli potrzebujesz +pełnej kontroli albo masz nietypowy przypadek. Provider to warstwa wygody nad tym. \ No newline at end of file diff --git a/src/exports/client.ts b/src/exports/client.ts index 4b1ab86..3780132 100644 --- a/src/exports/client.ts +++ b/src/exports/client.ts @@ -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' diff --git a/src/modules/turnstile/TurnstileProvider.tsx b/src/modules/turnstile/TurnstileProvider.tsx new file mode 100644 index 0000000..077662e --- /dev/null +++ b/src/modules/turnstile/TurnstileProvider.tsx @@ -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(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 + 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 + * {children} + * + * 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(null) + return ( + + {children} + + ) +} + +/** + * 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: + * // 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 ') + } + const { siteKey, token, setToken } = ctx + + const reset = useCallback(() => setToken(null), [setToken]) + + const TurnstileWidget = useCallback( + (props?: { theme?: 'light' | 'dark' | 'auto' }) => { + if (!siteKey) return null + return + }, + [siteKey, setToken], + ) + + return { token, TurnstileWidget, reset, enabled: Boolean(siteKey) } +} diff --git a/src/modules/turnstile/client.ts b/src/modules/turnstile/client.ts index b586876..2c52976 100644 --- a/src/modules/turnstile/client.ts +++ b/src/modules/turnstile/client.ts @@ -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' diff --git a/src/modules/turnstile/index.ts b/src/modules/turnstile/index.ts index 14ae88e..98b88e4 100644 --- a/src/modules/turnstile/index.ts +++ b/src/modules/turnstile/index.ts @@ -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'