├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── HISTORY.md
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── src
└── index.tsx
└── tsconfig.json
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches-ignore:
6 | - "dependabot/**"
7 | pull_request_target:
8 |
9 | jobs:
10 | lint:
11 | timeout-minutes: 5
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Checkout repo
15 | uses: actions/checkout@v3
16 |
17 | - name: Setup node
18 | uses: actions/setup-node@v3
19 | with:
20 | node-version: "22"
21 |
22 | - name: Install dependencies
23 | run: npm i
24 |
25 | - name: Linting
26 | run: npx -y prettier -c .
27 |
28 | - name: Type check
29 | run: npx -y tsc --strict
30 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | .pnpm-debug.log*
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
39 | build/Release
40 |
41 | # Dependency directories
42 | node_modules/
43 | jspm_packages/
44 |
45 | # Snowpack dependency directory (https://snowpack.dev/)
46 | web_modules/
47 |
48 | # TypeScript cache
49 | *.tsbuildinfo
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Optional stylelint cache
58 | .stylelintcache
59 |
60 | # Microbundle cache
61 | .rpt2_cache/
62 | .rts2_cache_cjs/
63 | .rts2_cache_es/
64 | .rts2_cache_umd/
65 |
66 | # Optional REPL history
67 | .node_repl_history
68 |
69 | # Output of 'npm pack'
70 | *.tgz
71 |
72 | # Yarn Integrity file
73 | .yarn-integrity
74 |
75 | # dotenv environment variable files
76 | .env
77 | .env.development.local
78 | .env.test.local
79 | .env.production.local
80 | .env.local
81 |
82 | # parcel-bundler cache (https://parceljs.org/)
83 | .cache
84 | .parcel-cache
85 |
86 | # Next.js build output
87 | .next
88 | out
89 |
90 | # Nuxt.js build / generate output
91 | .nuxt
92 | dist
93 |
94 | # Gatsby files
95 | .cache/
96 | # Comment in the public line in if your project uses Gatsby and not Next.js
97 | # https://nextjs.org/blog/next-9-1#public-directory-support
98 | # public
99 |
100 | # vuepress build output
101 | .vuepress/dist
102 |
103 | # vuepress v2.x temp and cache directory
104 | .temp
105 | .cache
106 |
107 | # Docusaurus cache and generated files
108 | .docusaurus
109 |
110 | # Serverless directories
111 | .serverless/
112 |
113 | # FuseBox cache
114 | .fusebox/
115 |
116 | # DynamoDB Local files
117 | .dynamodb/
118 |
119 | # TernJS port file
120 | .tern-port
121 |
122 | # Stores VSCode versions used for testing VSCode extensions
123 | .vscode-test
124 |
125 | # yarn v2
126 | .yarn/cache
127 | .yarn/unplugged
128 | .yarn/build-state.yml
129 | .yarn/install-state.gz
130 | .pnp.*
--------------------------------------------------------------------------------
/HISTORY.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7 |
8 | ## [Unreleased]
9 |
10 | ## [1.1.4] - 2024-09-24
11 |
12 | ### Added
13 |
14 | - `onSuccess` callback with the additional `preClearanceObtained` argument
15 | - `flexible` option for `size` prop
16 |
17 | ### Changed
18 |
19 | - Update `turnstile-types` to v1.2.3
20 |
21 | ## [1.1.3] - 2024-02-19
22 |
23 | ### Changed
24 |
25 | - React peer dependency version to v16.13.1
26 |
27 | ## [1.1.2] - 2023-09-18
28 |
29 | ### Added
30 |
31 | - `onAfterInteractive`, `onBeforeInteractive`, `onUnsupported` callbacks
32 |
33 | ### Changed
34 |
35 | - `onVerify` is no longer required
36 |
37 | ### Removed
38 |
39 | - Custom `retry` logic (issue fixed upstream in Turnstile itself)
40 |
41 | ### Fixed
42 |
43 | - Remount issue with certain callbacks (#15)
44 |
45 | ## [1.1.1] - 2023-06-25
46 |
47 | ### Added
48 |
49 | - `useTurnstile` hook
50 | - `fixedSize` option to reduce layout shift
51 | - Missing argument for `onExpire`
52 | - `BoundTurnstileObject` argument to callbacks
53 |
54 | ### Fixed
55 |
56 | - `global` -> `globalNamespace` (name conflict) (#10)
57 | - Implement `retry` logic ourselves (#14)
58 | - Missing `onError` error argument passthrough
59 |
60 | ## [1.1.0] - 2023-03-09
61 |
62 | ### Added
63 |
64 | - `refreshExpired` option
65 | - `language` option
66 | - `appearance` option
67 | - `execution` option
68 |
69 | ### Changed
70 |
71 | - Temporary load callback function is now removed after load
72 |
73 | ### Fixed
74 |
75 | - `onTimeout` callback not properly registering
76 | - `ref` not passing properly - pass `userRef` instead
77 | - `globalThis` errors on older browsers
78 |
79 | ### Removed
80 |
81 | - `autoResetOnExpire` - use `refreshExpired` instead
82 |
83 | ## [1.0.6] - 2022-11-25
84 |
85 | ### Added
86 |
87 | - Support for the new `onTimeout` callback
88 | - `autoResetOnExpire` for automatically resetting the Turnstile widget once the token expires
89 | - `retry` & `retryInterval`
90 | - custom `ref` argument for using your own ref (#7)
91 |
92 | ## [1.0.5] - 2022-10-22
93 |
94 | ### Added
95 |
96 | - require module support (#6)
97 | - `size` support
98 |
99 | ## [1.0.4] - 2022-10-11
100 |
101 | ### Changed
102 |
103 | - `onLoad` callback now includes the turnstile widget id (#5)
104 |
105 | ## [1.0.3] - 2022-10-05
106 |
107 | ### Fixed
108 |
109 | - Race condition by using `useRef` instead of `createRef` (#4)
110 |
111 | ## [1.0.2] - 2022-10-05
112 |
113 | ### Added
114 |
115 | - Experimental (undocumented) fields `responseField` and `responseFieldName` for controlling the `` element generated by Turnstile.
116 | - `style` prop which is directly passed to the internal `
`
117 |
118 | ### Changes
119 |
120 | - Using explicit rendering for Turnstile now which prevents [implicit renders](https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/#implicitly-render-the-turnstile-widget) (also undocumented)
121 | - Using `turnstile.remove()` to remove widgets after being unloaded
122 |
123 | ### Fixed
124 |
125 | - A race condition when loading Turnstile on page load in dev mode has been fixed
126 | - Callback props will now update as expected
127 | - Entrypoint pointing to the wrong file (#3)
128 |
129 | ## [1.0.1] - 2022-10-01
130 |
131 | ### Fixed
132 |
133 | - Fix double render in vite (#1)
134 | - Fix tslib being a dependency (#2)
135 |
136 | ## [1.0.0] - 2022-09-29
137 |
138 | Initial release.
139 |
140 | [unreleased]: https://github.com/Le0Developer/react-turnstile/compare/v1.1.4...HEAD
141 | [1.1.4]: https://github.com/le0developer/react-turnstile/compare/v1.1.3...v1.1.4
142 | [1.1.3]: https://github.com/le0developer/react-turnstile/compare/v1.1.2...v1.1.3
143 | [1.1.2]: https://github.com/le0developer/react-turnstile/compare/v1.1.1...v1.1.2
144 | [1.1.1]: https://github.com/le0developer/react-turnstile/compare/v1.1.0...v1.1.1
145 | [1.1.0]: https://github.com/le0developer/react-turnstile/compare/v1.0.6...v1.1.0
146 | [1.0.6]: https://github.com/le0developer/react-turnstile/compare/v1.0.5...v1.0.6
147 | [1.0.5]: https://github.com/le0developer/react-turnstile/compare/v1.0.4...v1.0.5
148 | [1.0.4]: https://github.com/le0developer/react-turnstile/compare/v1.0.3...v1.0.4
149 | [1.0.3]: https://github.com/le0developer/react-turnstile/compare/v1.0.2...v1.0.3
150 | [1.0.2]: https://github.com/le0developer/react-turnstile/compare/v1.0.1...v1.0.2
151 | [1.0.1]: https://github.com/le0developer/react-turnstile/compare/v1.0.0...v1.0.1
152 | [1.0.0]: https://github.com/Le0Developer/react-turnstile/releases/tag/v1.0.0
153 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022-2023 LeoDeveloper
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-turnstile
2 |
3 | A very simple React library for [Cloudflare Turnstile](https://challenges.cloudflare.com).
4 |
5 | ## Installation
6 |
7 | ```sh
8 | npm i react-turnstile
9 | ```
10 |
11 | ## Usage
12 |
13 | ```jsx
14 | import Turnstile, { useTurnstile } from "react-turnstile";
15 |
16 | // ...
17 |
18 | function TurnstileWidget() {
19 | const turnstile = useTurnstile();
20 | return (
21 | {
24 | fetch("/login", {
25 | method: "POST",
26 | body: JSON.stringify({ token }),
27 | }).then((response) => {
28 | if (!response.ok) turnstile.reset();
29 | });
30 | }}
31 | />
32 | );
33 | }
34 | ```
35 |
36 | Turnstile tokens expire after 5 minutes, to automatically reset the challenge once they expire,
37 | set the `refreshExpired` prop to `'auto'` or reset the widget yourself using the `onExpire` callback.
38 |
39 | ### Reducing Layout Shift
40 |
41 | The turnstile iframe initially loads as invisible before becoming visible and
42 | expanding to the expected widget size.
43 |
44 | This causes Layout Shift and reduces your Cumulative Layout Shift score and UX.
45 |
46 | This can be fixed with the `fixedSize={true}` option, which will force the
47 | wrapper div to be the specific size of turnstile.
48 |
49 | ### Bound Turnstile Object
50 |
51 | The Bound Turnstile Object is given as argument to all callbacks and allows you
52 | to call certain `window.turnstile` functions without having to store the `widgetId`
53 | yourself.
54 |
55 | ```js
56 | function Component() {
57 | return (
58 | {
61 | // before:
62 | window.turnstile.execute(widgetId);
63 | // now:
64 | bound.execute();
65 | }}
66 | />
67 | );
68 | }
69 | ```
70 |
71 | ## Documentation
72 |
73 | Turnstile takes the following arguments:
74 |
75 | | name | type | description |
76 | | ----------------- | ------- | ---------------------------------------------------- |
77 | | sitekey | string | sitekey of your website (REQUIRED) |
78 | | action | string | - |
79 | | cData | string | - |
80 | | theme | string | one of "light", "dark", "auto" |
81 | | language | string | override the language used by turnstile |
82 | | tabIndex | number | - |
83 | | responseField | boolean | controls generation of `` element |
84 | | responseFieldName | string | changes the name of `` element |
85 | | size | string | one of "normal", "compact", "flexible" |
86 | | fixedSize | boolean | fix the size of the `` to reduce layout shift |
87 | | retry | string | one of "auto", "never" |
88 | | retryInterval | number | interval of retries in ms |
89 | | refreshExpired | string | one of "auto", "manual", "never" |
90 | | appearance | string | one of "always", "execute", "interaction-only" |
91 | | execution | string | one of "render", "execute" |
92 | | id | string | id of the div |
93 | | userRef | Ref | custom react ref for the div |
94 | | className | string | passed to the div |
95 | | style | object | passed to the div |
96 |
97 | And the following callbacks:
98 |
99 | | name | arguments | description |
100 | | ------------------- | --------------------------- | --------------------------------------------------- |
101 | | onVerify | token | called when challenge is passed |
102 | | onSuccess | token, preClearanceObtained | called when challenge is passed |
103 | | onLoad | widgetId | called when the widget is loaded |
104 | | onError | error | called when an error occurs |
105 | | onExpire | - | called when the token expires |
106 | | onTimeout | token | called when the challenge expires |
107 | | onAfterInteractive | - | called when the challenge becomes interactive |
108 | | onBeforeInteractive | - | called when the challenge no longer is interactive |
109 | | onUnsupported | - | called when the browser is unsupported by Turnstile |
110 |
111 | The callbacks also take an additional `BoundTurnstileObject` which exposes
112 | certain functions of `window.turnstile` which are already bound to the
113 | current widget, so you don't need track the `widgetId` yourself.
114 |
115 | For more details on what each argument does, see the [Cloudflare Documentation](https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/#configurations).
116 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-turnstile",
3 | "version": "1.1.3",
4 | "lockfileVersion": 2,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "react-turnstile",
9 | "version": "1.1.3",
10 | "license": "MIT",
11 | "devDependencies": {
12 | "@types/react": "^18.0.21",
13 | "prettier": "^2.7.1",
14 | "turnstile-types": "^1.2.3",
15 | "typescript": "^4.8.4"
16 | },
17 | "peerDependencies": {
18 | "react": ">= 16.13.1",
19 | "react-dom": ">= 16.13.1"
20 | }
21 | },
22 | "node_modules/@types/prop-types": {
23 | "version": "15.7.5",
24 | "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
25 | "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==",
26 | "dev": true
27 | },
28 | "node_modules/@types/react": {
29 | "version": "18.0.21",
30 | "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.21.tgz",
31 | "integrity": "sha512-7QUCOxvFgnD5Jk8ZKlUAhVcRj7GuJRjnjjiY/IUBWKgOlnvDvTMLD4RTF7NPyVmbRhNrbomZiOepg7M/2Kj1mA==",
32 | "dev": true,
33 | "dependencies": {
34 | "@types/prop-types": "*",
35 | "@types/scheduler": "*",
36 | "csstype": "^3.0.2"
37 | }
38 | },
39 | "node_modules/@types/scheduler": {
40 | "version": "0.16.2",
41 | "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
42 | "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
43 | "dev": true
44 | },
45 | "node_modules/csstype": {
46 | "version": "3.1.1",
47 | "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
48 | "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==",
49 | "dev": true
50 | },
51 | "node_modules/js-tokens": {
52 | "version": "4.0.0",
53 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
54 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
55 | "peer": true
56 | },
57 | "node_modules/loose-envify": {
58 | "version": "1.4.0",
59 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
60 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
61 | "peer": true,
62 | "dependencies": {
63 | "js-tokens": "^3.0.0 || ^4.0.0"
64 | },
65 | "bin": {
66 | "loose-envify": "cli.js"
67 | }
68 | },
69 | "node_modules/prettier": {
70 | "version": "2.7.1",
71 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz",
72 | "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==",
73 | "dev": true,
74 | "bin": {
75 | "prettier": "bin-prettier.js"
76 | },
77 | "engines": {
78 | "node": ">=10.13.0"
79 | },
80 | "funding": {
81 | "url": "https://github.com/prettier/prettier?sponsor=1"
82 | }
83 | },
84 | "node_modules/react": {
85 | "version": "18.2.0",
86 | "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
87 | "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
88 | "peer": true,
89 | "dependencies": {
90 | "loose-envify": "^1.1.0"
91 | },
92 | "engines": {
93 | "node": ">=0.10.0"
94 | }
95 | },
96 | "node_modules/react-dom": {
97 | "version": "18.2.0",
98 | "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
99 | "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
100 | "peer": true,
101 | "dependencies": {
102 | "loose-envify": "^1.1.0",
103 | "scheduler": "^0.23.0"
104 | },
105 | "peerDependencies": {
106 | "react": "^18.2.0"
107 | }
108 | },
109 | "node_modules/scheduler": {
110 | "version": "0.23.0",
111 | "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
112 | "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
113 | "peer": true,
114 | "dependencies": {
115 | "loose-envify": "^1.1.0"
116 | }
117 | },
118 | "node_modules/turnstile-types": {
119 | "version": "1.2.3",
120 | "resolved": "https://registry.npmjs.org/turnstile-types/-/turnstile-types-1.2.3.tgz",
121 | "integrity": "sha512-EDjhDB9TDwda2JRbhzO/kButPio3JgrC3gXMVAMotxldybTCJQVMvPNJ89rcAiN9vIrCb2i1E+VNBCqB8wue0A==",
122 | "dev": true,
123 | "license": "MIT"
124 | },
125 | "node_modules/typescript": {
126 | "version": "4.8.4",
127 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz",
128 | "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==",
129 | "dev": true,
130 | "bin": {
131 | "tsc": "bin/tsc",
132 | "tsserver": "bin/tsserver"
133 | },
134 | "engines": {
135 | "node": ">=4.2.0"
136 | }
137 | }
138 | },
139 | "dependencies": {
140 | "@types/prop-types": {
141 | "version": "15.7.5",
142 | "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
143 | "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==",
144 | "dev": true
145 | },
146 | "@types/react": {
147 | "version": "18.0.21",
148 | "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.21.tgz",
149 | "integrity": "sha512-7QUCOxvFgnD5Jk8ZKlUAhVcRj7GuJRjnjjiY/IUBWKgOlnvDvTMLD4RTF7NPyVmbRhNrbomZiOepg7M/2Kj1mA==",
150 | "dev": true,
151 | "requires": {
152 | "@types/prop-types": "*",
153 | "@types/scheduler": "*",
154 | "csstype": "^3.0.2"
155 | }
156 | },
157 | "@types/scheduler": {
158 | "version": "0.16.2",
159 | "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
160 | "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
161 | "dev": true
162 | },
163 | "csstype": {
164 | "version": "3.1.1",
165 | "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
166 | "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==",
167 | "dev": true
168 | },
169 | "js-tokens": {
170 | "version": "4.0.0",
171 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
172 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
173 | "peer": true
174 | },
175 | "loose-envify": {
176 | "version": "1.4.0",
177 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
178 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
179 | "peer": true,
180 | "requires": {
181 | "js-tokens": "^3.0.0 || ^4.0.0"
182 | }
183 | },
184 | "prettier": {
185 | "version": "2.7.1",
186 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz",
187 | "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==",
188 | "dev": true
189 | },
190 | "react": {
191 | "version": "18.2.0",
192 | "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
193 | "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
194 | "peer": true,
195 | "requires": {
196 | "loose-envify": "^1.1.0"
197 | }
198 | },
199 | "react-dom": {
200 | "version": "18.2.0",
201 | "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
202 | "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
203 | "peer": true,
204 | "requires": {
205 | "loose-envify": "^1.1.0",
206 | "scheduler": "^0.23.0"
207 | }
208 | },
209 | "scheduler": {
210 | "version": "0.23.0",
211 | "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
212 | "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
213 | "peer": true,
214 | "requires": {
215 | "loose-envify": "^1.1.0"
216 | }
217 | },
218 | "turnstile-types": {
219 | "version": "1.2.3",
220 | "resolved": "https://registry.npmjs.org/turnstile-types/-/turnstile-types-1.2.3.tgz",
221 | "integrity": "sha512-EDjhDB9TDwda2JRbhzO/kButPio3JgrC3gXMVAMotxldybTCJQVMvPNJ89rcAiN9vIrCb2i1E+VNBCqB8wue0A==",
222 | "dev": true
223 | },
224 | "typescript": {
225 | "version": "4.8.4",
226 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz",
227 | "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==",
228 | "dev": true
229 | }
230 | }
231 | }
232 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-turnstile",
3 | "version": "1.1.4",
4 | "description": "React library for Cloudflare's Turnstile CAPTCHA alternative",
5 | "main": "dist/index.js",
6 | "scripts": {
7 | "prepublishOnly": "npm run build",
8 | "build": "tsc",
9 | "style-fix": "npx prettier -w ."
10 | },
11 | "exports": {
12 | "import": "./dist/index.js",
13 | "require": "./dist/index.js",
14 | "types": "./dist/index.d.ts"
15 | },
16 | "types": "./dist/index.d.ts",
17 | "repository": {
18 | "type": "git",
19 | "url": "https://github.com/Le0developer/react-turnstile"
20 | },
21 | "peerDependencies": {
22 | "react": ">= 16.13.1",
23 | "react-dom": ">= 16.13.1"
24 | },
25 | "author": "Leo Developer",
26 | "license": "MIT",
27 | "devDependencies": {
28 | "@types/react": "^18.0.21",
29 | "prettier": "^2.7.1",
30 | "turnstile-types": "^1.2.3",
31 | "typescript": "^4.8.4"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState, useRef } from "react";
2 | import {
3 | TurnstileObject,
4 | SupportedLanguages,
5 | RenderParameters,
6 | } from "turnstile-types";
7 |
8 | const globalNamespace = (
9 | typeof globalThis !== "undefined" ? globalThis : window
10 | ) as any;
11 | let turnstileState =
12 | typeof globalNamespace.turnstile !== "undefined" ? "ready" : "unloaded";
13 | let ensureTurnstile: () => Promise;
14 |
15 | // Functions responsible for loading the turnstile api, while also making sure
16 | // to only load it once
17 | let turnstileLoad: {
18 | resolve: (value?: any) => void;
19 | reject: (reason?: any) => void;
20 | };
21 | const turnstileLoadPromise = new Promise((resolve, reject) => {
22 | turnstileLoad = { resolve, reject };
23 | if (turnstileState === "ready") resolve(undefined);
24 | });
25 |
26 | {
27 | const TURNSTILE_LOAD_FUNCTION = "cf__reactTurnstileOnLoad";
28 | const TURNSTILE_SRC = "https://challenges.cloudflare.com/turnstile/v0/api.js";
29 |
30 | ensureTurnstile = () => {
31 | if (turnstileState === "unloaded") {
32 | turnstileState = "loading";
33 | globalNamespace[TURNSTILE_LOAD_FUNCTION] = () => {
34 | turnstileLoad.resolve();
35 | turnstileState = "ready";
36 | delete globalNamespace[TURNSTILE_LOAD_FUNCTION];
37 | };
38 | const url = `${TURNSTILE_SRC}?onload=${TURNSTILE_LOAD_FUNCTION}&render=explicit`;
39 | const script = document.createElement("script");
40 | script.src = url;
41 | script.async = true;
42 | script.addEventListener("error", () => {
43 | turnstileLoad.reject("Failed to load Turnstile.");
44 | delete globalNamespace[TURNSTILE_LOAD_FUNCTION];
45 | });
46 | document.head.appendChild(script);
47 | }
48 | return turnstileLoadPromise;
49 | };
50 | }
51 |
52 | export default function Turnstile({
53 | id,
54 | className,
55 | style: customStyle,
56 | sitekey,
57 | action,
58 | cData,
59 | theme,
60 | language,
61 | tabIndex,
62 | responseField,
63 | responseFieldName,
64 | size,
65 | fixedSize,
66 | retry,
67 | retryInterval,
68 | refreshExpired,
69 | appearance,
70 | execution,
71 | userRef,
72 | onVerify,
73 | onSuccess,
74 | onLoad,
75 | onError,
76 | onExpire,
77 | onTimeout,
78 | onAfterInteractive,
79 | onBeforeInteractive,
80 | onUnsupported,
81 | }: TurnstileProps) {
82 | const ownRef = useRef(null);
83 | const inplaceState = useState({
84 | onVerify,
85 | onSuccess,
86 | onLoad,
87 | onError,
88 | onExpire,
89 | onTimeout,
90 | onAfterInteractive,
91 | onBeforeInteractive,
92 | onUnsupported,
93 | })[0];
94 |
95 | const ref = userRef ?? ownRef;
96 |
97 | const style = fixedSize
98 | ? {
99 | width:
100 | size === "compact" ? "130px" : size === "flexible" ? "100%" : "300px",
101 | height: size === "compact" ? "120px" : "65px",
102 | ...customStyle,
103 | }
104 | : customStyle;
105 |
106 | useEffect(() => {
107 | if (!ref.current) return;
108 | let cancelled = false;
109 | let widgetId = "";
110 | (async () => {
111 | // load turnstile
112 | if (turnstileState !== "ready") {
113 | try {
114 | await ensureTurnstile();
115 | } catch (e) {
116 | inplaceState.onError?.(e);
117 | return;
118 | }
119 | }
120 | if (cancelled || !ref.current) return;
121 | let boundTurnstileObject: BoundTurnstileObject;
122 | const turnstileOptions: RenderParameters = {
123 | sitekey,
124 | action,
125 | cData,
126 | theme,
127 | language,
128 | tabindex: tabIndex,
129 | "response-field": responseField,
130 | "response-field-name": responseFieldName,
131 | size,
132 | retry,
133 | "retry-interval": retryInterval,
134 | "refresh-expired": refreshExpired,
135 | appearance,
136 | execution,
137 | callback: (token: string, preClearanceObtained: boolean) => {
138 | inplaceState.onVerify?.(token, boundTurnstileObject);
139 | inplaceState.onSuccess?.(
140 | token,
141 | preClearanceObtained,
142 | boundTurnstileObject
143 | );
144 | },
145 | "error-callback": (error?: any) =>
146 | inplaceState.onError?.(error, boundTurnstileObject),
147 | "expired-callback": (token: string) =>
148 | inplaceState.onExpire?.(token, boundTurnstileObject),
149 | "timeout-callback": () =>
150 | inplaceState.onTimeout?.(boundTurnstileObject),
151 | "after-interactive-callback": () =>
152 | inplaceState.onAfterInteractive?.(boundTurnstileObject),
153 | "before-interactive-callback": () =>
154 | inplaceState.onBeforeInteractive?.(boundTurnstileObject),
155 | "unsupported-callback": () =>
156 | inplaceState.onUnsupported?.(boundTurnstileObject),
157 | };
158 |
159 | widgetId = window.turnstile.render(ref.current, turnstileOptions);
160 | boundTurnstileObject = createBoundTurnstileObject(widgetId);
161 | inplaceState.onLoad?.(widgetId, boundTurnstileObject);
162 | })();
163 | return () => {
164 | cancelled = true;
165 | if (widgetId) window.turnstile.remove(widgetId);
166 | };
167 | }, [
168 | sitekey,
169 | action,
170 | cData,
171 | theme,
172 | language,
173 | tabIndex,
174 | responseField,
175 | responseFieldName,
176 | size,
177 | retry,
178 | retryInterval,
179 | refreshExpired,
180 | appearance,
181 | execution,
182 | ]);
183 | useEffect(() => {
184 | inplaceState.onVerify = onVerify;
185 | inplaceState.onSuccess = onSuccess;
186 | inplaceState.onLoad = onLoad;
187 | inplaceState.onError = onError;
188 | inplaceState.onExpire = onExpire;
189 | inplaceState.onTimeout = onTimeout;
190 | inplaceState.onAfterInteractive = onAfterInteractive;
191 | inplaceState.onBeforeInteractive = onBeforeInteractive;
192 | inplaceState.onUnsupported = onUnsupported;
193 | }, [
194 | onVerify,
195 | onSuccess,
196 | onLoad,
197 | onError,
198 | onExpire,
199 | onTimeout,
200 | onAfterInteractive,
201 | onBeforeInteractive,
202 | onUnsupported,
203 | ]);
204 |
205 | return ;
206 | }
207 |
208 | export interface TurnstileProps extends TurnstileCallbacks {
209 | sitekey: string;
210 | action?: string;
211 | cData?: string;
212 | theme?: "light" | "dark" | "auto";
213 | language?: SupportedLanguages | "auto";
214 | tabIndex?: number;
215 | responseField?: boolean;
216 | responseFieldName?: string;
217 | size?: "normal" | "compact" | "flexible" | "invisible";
218 | fixedSize?: boolean;
219 | retry?: "auto" | "never";
220 | retryInterval?: number;
221 | refreshExpired?: "auto" | "manual" | "never";
222 | appearance?: "always" | "execute" | "interaction-only";
223 | execution?: "render" | "execute";
224 | id?: string;
225 | userRef?: React.MutableRefObject;
226 | className?: string;
227 | style?: React.CSSProperties;
228 | }
229 |
230 | export interface TurnstileCallbacks {
231 | onVerify?: (token: string, boundTurnstile: BoundTurnstileObject) => void;
232 | onSuccess?: (
233 | token: string,
234 | preClearanceObtained: boolean,
235 | boundTurnstile: BoundTurnstileObject
236 | ) => void;
237 | onLoad?: (widgetId: string, boundTurnstile: BoundTurnstileObject) => void;
238 | onError?: (
239 | error?: Error | any,
240 | boundTurnstile?: BoundTurnstileObject
241 | ) => void;
242 | onExpire?: (token: string, boundTurnstile: BoundTurnstileObject) => void;
243 | onTimeout?: (boundTurnstile: BoundTurnstileObject) => void;
244 | onAfterInteractive?: (boundTurnstile: BoundTurnstileObject) => void;
245 | onBeforeInteractive?: (boundTurnstile: BoundTurnstileObject) => void;
246 | onUnsupported?: (boundTurnstile: BoundTurnstileObject) => void;
247 | }
248 |
249 | export interface BoundTurnstileObject {
250 | execute: (options?: RenderParameters) => void;
251 | reset: () => void;
252 | getResponse: () => void;
253 | isExpired: () => boolean;
254 | }
255 |
256 | function createBoundTurnstileObject(widgetId: string): BoundTurnstileObject {
257 | return {
258 | execute: (options) => window.turnstile.execute(widgetId, options),
259 | reset: () => window.turnstile.reset(widgetId),
260 | getResponse: () => window.turnstile.getResponse(widgetId),
261 | isExpired: () => window.turnstile.isExpired(widgetId),
262 | };
263 | }
264 |
265 | export function useTurnstile(): TurnstileObject {
266 | // we are using state here to trigger a component re-render once turnstile
267 | // loads, so the component using this hook gets the object once its loaded
268 | const [_, setState] = useState(turnstileState);
269 |
270 | useEffect(() => {
271 | if (turnstileState === "ready") return;
272 | turnstileLoadPromise.then(() => setState(turnstileState));
273 | }, []);
274 |
275 | return globalNamespace.turnstile;
276 | }
277 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "CommonJS",
4 | "declaration": true,
5 | "sourceMap": true,
6 | "rootDir": "./src",
7 | "strict": true,
8 | "outDir": "./dist",
9 | "noUnusedLocals": true,
10 | "noUnusedParameters": true,
11 | "noImplicitReturns": true,
12 | "noFallthroughCasesInSwitch": true,
13 | "downlevelIteration": true,
14 | "moduleResolution": "node",
15 | "baseUrl": "./",
16 | "paths": {
17 | "*": ["src/*", "node_modules/*"]
18 | },
19 | "jsx": "react",
20 | "esModuleInterop": true,
21 | "target": "ES2019",
22 | "allowJs": true,
23 | "skipLibCheck": true,
24 | "forceConsistentCasingInFileNames": true,
25 | "resolveJsonModule": true,
26 | "isolatedModules": true,
27 | "types": ["turnstile-types"]
28 | },
29 | "exclude": ["node_modules", "dist"]
30 | }
31 |
--------------------------------------------------------------------------------