├── .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 | --------------------------------------------------------------------------------