├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ └── issue-template.md └── workflows │ └── standard-ci.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── MIGRATION.md ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── index.js ├── recaptcha-wrapper.js └── recaptcha.js └── test ├── .eslintrc ├── index.js ├── recaptcha-wrapper.spec.js └── recaptcha.spec.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/react", 4 | [ "@babel/env", { "loose": true } ] 5 | ], 6 | "env": { 7 | "esm": { 8 | "presets": [ 9 | [ "@babel/env", { "loose": true, "modules": false } ] 10 | ] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | [*.js] 12 | charset = utf-8 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [{package.json,.travis.yml}] 17 | indent_style = space 18 | indent_size = 2 19 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib/** 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* eslint-env node */ 3 | module.exports = { 4 | extends: ["eslint:recommended", "plugin:react/recommended", "prettier"], 5 | parserOptions: { 6 | sourceType: "module", 7 | ecmaVersion: "latest", 8 | ecmaFeatures: { 9 | jsx: true, 10 | }, 11 | }, 12 | env: { 13 | es6: true, 14 | browser: true, 15 | }, 16 | plugins: ["react", "prettier"], 17 | rules: { 18 | "prettier/prettier": "error", 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Issue template 3 | about: Standard Issue Template 4 | 5 | --- 6 | 7 | react-google-recaptcha version: X.X.X 8 | react-async-script version: Y.Y.Y 9 | -------------------------------------------------------------------------------- /.github/workflows/standard-ci.yml: -------------------------------------------------------------------------------- 1 | 2 | name: standard-ci 3 | 4 | on: 5 | pull_request: 6 | paths-ignore: 7 | - '**.md' 8 | push: 9 | branches: 10 | - master 11 | paths-ignore: 12 | - '**.md' 13 | 14 | jobs: 15 | test: 16 | strategy: 17 | matrix: 18 | node: [14, 16, 18] 19 | os: [ubuntu-latest, windows-latest, macos-latest] 20 | name: Node v${{ matrix.node }} - ${{ matrix.os }} 21 | runs-on: ${{ matrix.os }} 22 | steps: 23 | - uses: actions/checkout@v3 24 | 25 | - uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node }} 28 | cache: 'npm' 29 | 30 | - name: Install dependencies 31 | run: npm ci 32 | 33 | - name: Test 34 | run: npm test 35 | 36 | - name: Lint 37 | run: npm run lint 38 | 39 | - name: Build 40 | run: npm run build 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /lib/ 3 | *.log 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | test/ 3 | tools/ 4 | .gitignore 5 | .travis.yml 6 | karma.conf.js 7 | .babelrc 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "trailingComma": "all", 4 | "parser": "babel" 5 | } 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | v3.1.0 - Sat 3 Jun 2023 0:30 ET 2 | --------------------------------------- 3 | - Add Enterprise Support 1 [(#239)](https://github.com/dozoisch/react-google-recaptcha/issues/239) 4 | - Add Enterprise Support 2 [(#270)](https://github.com/dozoisch/react-google-recaptcha/issues/270) 5 | - Add Nonce Support [(#221)](https://github.com/dozoisch/react-google-recaptcha/issues/221) 6 | - Update dependencies 1 [(#206)](https://github.com/dozoisch/react-google-recaptcha/issues/206) 7 | - Update dependencies 2 [(#207)](https://github.com/dozoisch/react-google-recaptcha/issues/207) 8 | - Update dependencies 3 [(#210)](https://github.com/dozoisch/react-google-recaptcha/issues/210) 9 | - Update dependencies 4 [(#211)](https://github.com/dozoisch/react-google-recaptcha/issues/211) 10 | - Update dependencies 5 [(#219)](https://github.com/dozoisch/react-google-recaptcha/issues/219) 11 | - Update dependencies 6 [(#226)](https://github.com/dozoisch/react-google-recaptcha/issues/226) 12 | - Update dependencies 7 [(#229)](https://github.com/dozoisch/react-google-recaptcha/issues/229) 13 | - Update dependencies 8 [(#235)](https://github.com/dozoisch/react-google-recaptcha/issues/235) 14 | - Update dependencies 9 [(#245)](https://github.com/dozoisch/react-google-recaptcha/issues/245) 15 | 16 | 17 | v3.0.0-alpha.1 - Mon 23 Nov 2020 22:25 ET 18 | --------------------------------------- 19 | - Add isolated prop [(#179)](https://github.com/dozoisch/react-google-recaptcha/issues/179) 20 | - Update dependencies 1 [(#187)](https://github.com/dozoisch/react-google-recaptcha/issues/187) 21 | - Update dependencies 2 [(#194)](https://github.com/dozoisch/react-google-recaptcha/issues/194) 22 | - Remove Timeout Removal [(#196)](https://github.com/dozoisch/react-google-recaptcha/issues/196) 23 | 24 | 25 | 26 | v2.1.0 - Fri 5 Jun 2020 22:05 PST 27 | --------------------------------------- 28 | - Add promise based Execution [(#163)](https://github.com/dozoisch/react-google-recaptcha/issues/163) 29 | 30 | 31 | 32 | v2.0.1 - Sat 14 Sep 2019 12:00 PST 33 | --------------------------------------- 34 | 35 | - Fix onChange was using the function passed at mount time [(#154)](https://github.com/dozoisch/react-google-recaptcha/issues/154) 36 | 37 | 38 | 39 | v2.0.0-rc.1 - Sat 3 Aug 2019 8:00 PST 40 | --------------------------------------- 41 | 42 | - Remove lang and removeOnUnmount global options [(#143)](https://github.com/dozoisch/react-google-recaptcha/issues/143) 43 | - Upgrade to node 8, 10, 12 [(#143)](https://github.com/dozoisch/react-google-recaptcha/issues/143) 44 | - Update to react-async-script 1.1 [(#143)](https://github.com/dozoisch/react-google-recaptcha/issues/143) 45 | 46 | 47 | v1.1.0 - Sun 14 Jul 2019 10:56 PST 48 | --------------------------------------- 49 | 50 | - Add HL prop [(#141)](https://github.com/dozoisch/react-google-recaptcha/issues/141) 51 | - Upgrade babel and jest [(#140)](https://github.com/dozoisch/react-google-recaptcha/issues/140) 52 | 53 | 54 | 55 | v1.0.5 - Mon 12 Nov 2018 9:25 PST 56 | --------------------------------------- 57 | 58 | - Fix promise timeout [(#120)](https://github.com/dozoisch/react-google-recaptcha/issues/120) 59 | 60 | 61 | 62 | v1.0.4 - Thu 27 Sep 2018 15:14 PST 63 | --------------------------------------- 64 | 65 | - add .babelrc to .npmignore [(#111)](https://github.com/dozoisch/react-google-recaptcha/issues/111) 66 | 67 | 68 | 69 | v1.0.2 - Wed, 5 Sep 2018 13:21:00 EST 70 | --------------------------------------- 71 | 72 | - fixe unbound onErrored handler [(#104)](https://github.com/dozoisch/react-google-recaptcha/pull/104) 73 | 74 | 75 | 76 | v1.0.1 - Thu, 30 Aug 2018 12:54:00 EST 77 | --------------------------------------- 78 | 79 | - Make the onChange prop not required [(#102)](https://github.com/dozoisch/react-google-recaptcha/pull/102) 80 | 81 | 82 | 83 | v1.0.0 - Fri, 17 Aug 2018 18:11:00 PST 84 | --------------------------------------- 85 | 86 | - Update to react-async-script 1.0 [(#94)](https://github.com/dozoisch/react-google-recaptcha/pull/94) 87 | - Add on Error [(#97)](https://github.com/dozoisch/react-google-recaptcha/pull/97) 88 | - Update build tools 89 | - Node 8 [(#95)](https://github.com/dozoisch/react-google-recaptcha/pull/95) 90 | - Jest [(#95)](https://github.com/dozoisch/react-google-recaptcha/pull/95) 91 | - Eslint/Prettier [(#96)](https://github.com/dozoisch/react-google-recaptcha/pull/96) 92 | 93 | 94 | 95 | v0.14.0 - Sun, 29 Jul 2018 19:20:03 GMT 96 | --------------------------------------- 97 | 98 | - [701695b](../../commit/701695b) [changed] dynamic url creation to allow language change 99 | 100 | 101 | 102 | v0.13.0 - Tue, 24 Jul 2018 21:50:35 GMT 103 | --------------------------------------- 104 | 105 | - [fde0d51](../../commit/fde0d51) [fixed] Update async-script to get rid of Map polyfill 106 | 107 | 108 | 109 | v0.12.0 - Mon, 11 Jun 2018 16:39:53 GMT 110 | --------------------------------------- 111 | 112 | - [f465421](../../commit/f465421) [fixed] react-async-script to dependency 113 | - [48a5726](../../commit/48a5726) [added] Parameter to use recaptchanet instead of google.com 114 | 115 | 116 | 117 | v0.11.1 - Thu, 10 May 2018 22:28:19 GMT 118 | --------------------------------------- 119 | 120 | - [b2ae438](../../commit/b2ae438) [fixed] issue grecaptcha.render is not a function by adding condition 121 | 122 | 123 | 124 | v0.11.0 - Sun, 25 Mar 2018 20:09:27 GMT 125 | --------------------------------------- 126 | 127 | - [4920312](../../commit/4920312) Add es2015 module build (#59) 128 | 129 | 130 | 131 | 132 | v0.10.0 - Sun, 25 Mar 2018 20:02:26 GMT 133 | --------------------------------------- 134 | 135 | - [8c6a8dc](../../commit/8c6a8dc) Remove rendered DOM on unmount (#73) 136 | 137 | 138 | 139 | v0.9.8 - Tue, 28 Nov 2017 04:07:05 GMT 140 | -------------------------------------- 141 | 142 | - [7cd1254](../../commit/7cd1254) Fix peerDependency of react 143 | 144 | 145 | 146 | 147 | v0.9.7 - Thu, 10 Aug 2017 00:04:43 GMT 148 | -------------------------------------- 149 | 150 | - [e5f6fd9](../../commit/e5f6fd9) Add an accessor for widgetid (#53) 151 | - [8932900](../../commit/8932900) Fixed PropTypes warning due to extra space (#51) 152 | 153 | 154 | 155 | v0.9.6 - Tue, 20 Jun 2017 16:20:26 GMT 156 | -------------------------------------- 157 | 158 | - [9796adb](../../commit/9796adb) Fix race on execute() (#49) 159 | 160 | 161 | 162 | 163 | v0.9.5 - Fri, 02 Jun 2017 09:26:50 GMT 164 | -------------------------------------- 165 | 166 | - Fixed release 167 | 168 | 169 | v0.9.4 - Fri, 02 Jun 2017 09:26:02 GMT 170 | -------------------------------------- 171 | 172 | - [89a1c0e](../../commit/89a1c0e) Changed ref to use function instead of dep strings 173 | - [5c8a04d](../../commit/5c8a04d) Update examples in readme (#47) 174 | - [349aaa8](../../commit/349aaa8) Modified description of badge parameter (#46) 175 | 176 | 177 | 178 | v0.9.3 - Mon, 24 Apr 2017 16:31:56 GMT 179 | -------------------------------------- 180 | 181 | - [cb041a3](../../commit/cb041a3) [fixed] issue with handleExpired not being bound 182 | 183 | 184 | 185 | v0.9.2 - Thu, 20 Apr 2017 01:10:09 GMT 186 | -------------------------------------- 187 | 188 | - [1c8d411](../../commit/1c8d411) [fixed] updated async-script 189 | 190 | 191 | 192 | v0.9.1 - Tue, 18 Apr 2017 08:27:28 GMT 193 | -------------------------------------- 194 | 195 | - [242faf5](../../commit/242faf5) [fixed] space in peer dep version 196 | 197 | 198 | 199 | v0.9.0 - Sun, 16 Apr 2017 23:36:41 GMT 200 | -------------------------------------- 201 | 202 | - [bbf5312](../../commit/bbf5312) [changed] updated for react 15.5 203 | 204 | 205 | 206 | v0.8.1 - Mon, 03 Apr 2017 04:34:00 GMT 207 | -------------------------------------- 208 | 209 | - [dbd3a47](../../commit/dbd3a47) [fixed] react-async-script dep version 210 | 211 | 212 | 213 | v0.8.0 - Fri, 24 Mar 2017 00:54:19 GMT 214 | -------------------------------------- 215 | 216 | - [883210e](../../commit/883210e) Added support for badge attribute (#36) 217 | 218 | 219 | 220 | 221 | v0.7.0 - Thu, 16 Mar 2017 17:11:15 GMT 222 | -------------------------------------- 223 | 224 | - [4eda897](../../commit/4eda897) Added Invisible example to README.md 225 | - [34d5e0c](../../commit/34d5e0c) Add invisible props and execute method (#34) 226 | - [cbfe092](../../commit/cbfe092) [added] install of react-async-script in readme 227 | 228 | 229 | 230 | v0.6.0 - Sun, 05 Mar 2017 02:45:00 GMT 231 | -------------------------------------- 232 | 233 | - [3c92d6d](../../commit/3c92d6d) [changed] Updated babel, react and dropped 0.10, 0.12 (#30) 234 | - [7c8424c](../../commit/7c8424c) make dependencies more friendly for consuming packages (#29) 235 | 236 | 237 | 238 | v0.5.4 - Tue, 19 Jul 2016 21:46:12 GMT 239 | -------------------------------------- 240 | 241 | - [cb679d7](../../commit/cb679d7) [fixed] issue with react 15.2 warnings for unknown props 242 | 243 | 244 | 245 | v0.5.3 - Tue, 03 May 2016 01:57:36 GMT 246 | -------------------------------------- 247 | 248 | - [50e770f](../../commit/50e770f) [added] temporary solution to the lang issue 249 | 250 | 251 | 252 | v0.5.2 - Sat, 28 Nov 2015 00:47:54 GMT 253 | -------------------------------------- 254 | 255 | - [0e94a8a](../../commit/0e94a8a) [added] stoken parameter 256 | 257 | 258 | 259 | v0.5.1 - Fri, 06 Nov 2015 16:17:16 GMT 260 | -------------------------------------- 261 | 262 | - [b7cfa5c](../../commit/b7cfa5c) [fixed] handle widgetId equal to 0 263 | 264 | 265 | 266 | v0.5.0 - Thu, 15 Oct 2015 21:38:43 GMT 267 | -------------------------------------- 268 | 269 | - [d217dd1](../../commit/d217dd1) [changed] updated all deps 270 | - [fc3350a](../../commit/fc3350a) [added] mt-changelog and release-script 271 | - [dfa9bf2](../../commit/dfa9bf2) [added] doc for react 0.14 + old 0.13 272 | 273 | 274 | 275 | # 0.4.0 276 | - Added Size Props |Merge #5 277 | - Fixed bug with refs 278 | - Bumps deps 279 | 280 | ## 0.3.2 281 | - Bump deps 282 | 283 | ## 0.3.1 284 | - Added babel runtime to deps 285 | - [#1] Removed unused use strict 286 | - Bump deps 287 | 288 | ## 0.3.0 289 | - Can now uses the recaptcha functions `getValue` `reset` directly from Wrapper without `getComponent` first. 290 | - bump deps 291 | 292 | ## 0.2.2 293 | - Bump react-async-script (bug 0.2.1) 294 | 295 | ## 0.2.1 296 | - Small lint fixes 297 | - Updated react-async-script 298 | 299 | ## 0.2.0 300 | - Now loads the script using react-async-script! Usage is now way simpler 301 | 302 | ## 0.1.0 303 | - Migrated to ES6 304 | - Added Travis 305 | - Added Babel 306 | - Added Karma 307 | - Added First test 308 | 309 | ## 0.0.1 310 | - Initial commit of component 311 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Hugo Dozois 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. 22 | 23 | -------------------------------------------------------------------------------- /MIGRATION.md: -------------------------------------------------------------------------------- 1 | # Migrations 2 | 3 | ## Migrate from 1.0 to 2.0 4 | 5 | - __options.removeOnUnmount__: *REMOVED* This was only useful for the lang changes. Lang is now changed through the `hl` prop. 6 | - __options.lang__: *REMOVED* Instead pass it as the `hl` prop on the component. 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-google-recaptcha 2 | 3 | [![Build Status][ci.img]][ci.url] [![npm version][npm.img]][npm.url] [![npm downloads][npm.dl.img]][npm.dl.url] 4 | 5 | [![Edit react-google-recaptcha example](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/1y4zzjq37l) 6 | 7 | React component for [Google reCAPTCHA v2][reCAPTCHA]. 8 | 9 | ## Installation 10 | 11 | ```shell 12 | npm install --save react-google-recaptcha 13 | ``` 14 | 15 | ## Usage 16 | 17 | All you need to do is [sign up for an API key pair][signup]. You will need the client key then you can use ``. 18 | 19 | The default usage imports a wrapped component that loads the google recaptcha script asynchronously then instantiates a `reCAPTCHA` the user can then interact with. 20 | 21 | Code Example: 22 | ```jsx 23 | import ReCAPTCHA from "react-google-recaptcha"; 24 | 25 | function onChange(value) { 26 | console.log("Captcha value:", value); 27 | } 28 | 29 | ReactDOM.render( 30 | , 34 | document.body 35 | ); 36 | ``` 37 | 38 | ### Component Props 39 | 40 | Properties used to customise the rendering: 41 | 42 | | Name | Type | Description | 43 | |:---- | ---- | ------ | 44 | | asyncScriptOnLoad | func | *optional* callback when the google recaptcha script has been loaded | 45 | | badge | enum | *optional* `bottomright`, `bottomleft` or `inline`. Positions reCAPTCHA badge. *Only for invisible reCAPTCHA* | 46 | | hl | string | *optional* set the hl parameter, which allows the captcha to be used from different languages, see [reCAPTCHA hl] | 47 | | isolated | bool | *optional* For plugin owners to not interfere with existing reCAPTCHA installations on a page. If true, this reCAPTCHA instance will be part of a separate ID space. *(__default:__ `false`)* 48 | | onChange | func | The function to be called when the user successfully completes the captcha | 49 | | onErrored | func | *optional* callback when the challenge errored, most likely due to network issues. | 50 | | onExpired | func | *optional* callback when the challenge is expired and has to be redone by user. By default it will call the onChange with null to signify expired callback. | 51 | | sitekey | string | The API client key | 52 | | size | enum | *optional* `compact`, `normal` or `invisible`. This allows you to change the size or do an invisible captcha | 53 | | stoken | string | *optional* set the stoken parameter, which allows the captcha to be used from different domains, see [reCAPTCHA secure-token] | 54 | | tabindex | number | *optional* The tabindex on the element *(__default:__ `0`)* 55 | | type | enum | *optional* `image` or `audio` The type of initial captcha *(__defaults:__ `image`)* 56 | | theme | enum | *optional* `light` or `dark` The theme of the widget *(__defaults:__ `light`)*. See [example][docs_theme] 57 | 58 | ### Component Instance API 59 | 60 | The component instance also has some utility functions that can be called. These can be accessed via `ref`. 61 | 62 | - `getValue()` returns the value of the captcha field 63 | - `getWidgetId()` returns the recaptcha widget Id 64 | - `reset()` forces reset. See the [JavaScript API doc][js_api] 65 | - `execute()` programmatically invoke the challenge 66 | - need to call when using `"invisible"` reCAPTCHA - [example below](#invisible-recaptcha) 67 | - `executeAsync()` programmatically invoke the challenge and return a promise that resolves to the token or errors(if encountered). 68 | - alternative approach to `execute()` in combination with the `onChange()` prop - [example below](#invisible-recaptcha) 69 | 70 | Example: 71 | ```javascript 72 | const recaptchaRef = React.createRef(); 73 | ... 74 | onSubmit = () => { 75 | const recaptchaValue = recaptchaRef.current.getValue(); 76 | this.props.onSubmit(recaptchaValue); 77 | } 78 | render() { 79 | return ( 80 |
81 | 86 | 87 | ) 88 | } 89 | ``` 90 | 91 | ### Invisible reCAPTCHA 92 | 93 | [▶ Codesandbox invisible example](https://codesandbox.io/s/gifted-cache-10q74jj593) 94 | 95 | See the [reCAPTCHA documentation](https://developers.google.com/recaptcha/docs/invisible) to see how to configure it. 96 | 97 | With the invisible option, you need to handle things a bit differently. You will need to call the `execute` method yourself. 98 | 99 | ```jsx 100 | import ReCAPTCHA from "react-google-recaptcha"; 101 | 102 | const recaptchaRef = React.createRef(); 103 | 104 | ReactDOM.render( 105 |
{ recaptchaRef.current.execute(); }}> 106 | 112 | , 113 | document.body 114 | ); 115 | ``` 116 | 117 | Additionally, you can use the `executeAsync` method to use a promise based approach. 118 | 119 | ```jsx 120 | import ReCAPTCHA from "react-google-recaptcha"; 121 | 122 | 123 | const ReCAPTCHAForm = (props) => { 124 | const recaptchaRef = React.useRef(); 125 | 126 | const onSubmitWithReCAPTCHA = async () => { 127 | const token = await recaptchaRef.current.executeAsync(); 128 | 129 | // apply to form data 130 | } 131 | 132 | return ( 133 |
134 | 139 | 140 | ) 141 | 142 | } 143 | 144 | ReactDOM.render( 145 | , 146 | document.body 147 | ); 148 | ``` 149 | 150 | 151 | ### Advanced usage 152 | 153 | #### Global properties used by reCaptcha 154 | 155 | __useRecaptchaNet__: If google.com is blocked, you can set `useRecaptchaNet` to `true` so that the component uses recaptcha.net instead. 156 | 157 | __enterprise__: if you want to use Google Enterprise Recaptcha, instead of the free version, set `enterprise` to `true`. 158 | 159 | Example global properties: 160 | ```js 161 | window.recaptchaOptions = { 162 | useRecaptchaNet: true, 163 | enterprise: true, 164 | }; 165 | ``` 166 | 167 | ### CSP Nonce support 168 | ```js 169 | window.recaptchaOptions = { 170 | nonce: document.querySelector('meta[name=\'csp-nonce\']').getAttribute('content'), 171 | }; 172 | ``` 173 | 174 | #### ReCaptcha loading google recaptcha script manually 175 | 176 | You can also use the barebone components doing the following. Using that component will oblige you to manage the grecaptcha dep and load the script by yourself. 177 | 178 | ```jsx 179 | import { ReCAPTCHA } from "react-google-recaptcha"; 180 | 181 | const grecaptchaObject = window.grecaptcha // You must provide access to the google grecaptcha object. 182 | 183 | render( 184 | this.recaptcha = r} 186 | sitekey="Your client site key" 187 | grecaptcha={grecaptchaObject} 188 | />, 189 | document.body 190 | ); 191 | ``` 192 | 193 | #### Hiding the Recaptcha 194 | 195 | According to the [google docs](https://developers.google.com/recaptcha/docs/faq#id-like-to-hide-the-recaptcha-badge.-what-is-allowed) you are allowed to hide the badge as long as you include the reCAPTCHA branding visibly in the user flow. Please include the following text: 196 | 197 | ``` 198 | This site is protected by reCAPTCHA and the Google 199 | Privacy Policy and 200 | Terms of Service apply. 201 | ``` 202 | 203 | If you wish to hide the badge you must add: 204 | 205 | ``` 206 | .grecaptcha-badge { visibility: hidden; } 207 | 208 | ``` 209 | 210 | to your css. 211 | 212 | 213 | 214 | [ci.img]: https://github.com/dozoisch/react-google-recaptcha/actions/workflows/standard-ci.yml/badge.svg?branch=master 215 | [ci.url]: https://github.com/dozoisch/react-google-recaptcha/actions/workflows/standard-ci.yml 216 | [npm.img]: https://badge.fury.io/js/react-google-recaptcha.svg 217 | [npm.url]: http://badge.fury.io/js/react-google-recaptcha 218 | [npm.dl.img]: https://img.shields.io/npm/dm/react-google-recaptcha.svg 219 | [npm.dl.url]: https://www.npmjs.com/package/react-google-recaptcha 220 | 221 | [reCAPTCHA]: https://developers.google.com/recaptcha/docs/display 222 | [signup]: http://www.google.com/recaptcha/admin 223 | [docs]: https://developers.google.com/recaptcha 224 | [docs_theme]: https://developers.google.com/recaptcha/docs/faq#can-i-customize-the-recaptcha-widget 225 | [js_api]: https://developers.google.com/recaptcha/docs/display#js_api 226 | [rb]: https://github.com/react-bootstrap/react-bootstrap/ 227 | [reCAPTCHA secure-token]: https://developers.google.com/recaptcha/docs/secure_token 228 | [reCAPTCHA hl]: https://developers.google.com/recaptcha/docs/language 229 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | "use strict"; 3 | // For a detailed explanation regarding each configuration property, visit: 4 | // https://jestjs.io/docs/en/configuration.html 5 | 6 | module.exports = { 7 | // All imported modules in your tests should be mocked automatically 8 | // automock: false, 9 | 10 | // Stop running tests after the first failure 11 | // bail: false, 12 | 13 | // Respect "browser" field in package.json when resolving modules 14 | // browser: false, 15 | 16 | // The directory where Jest should store its cached dependency information 17 | // cacheDirectory: "/var/folders/l7/cx7wgg653n7619h3gf_zjjzr0000gn/T/jest_dx", 18 | 19 | // Automatically clear mock calls and instances between every test 20 | clearMocks: true, 21 | 22 | // Indicates whether the coverage information should be collected while executing the test 23 | // collectCoverage: false, 24 | 25 | // An array of glob patterns indicating a set of files for which coverage information should be collected 26 | // collectCoverageFrom: null, 27 | 28 | // The directory where Jest should output its coverage files 29 | // coverageDirectory: null, 30 | 31 | // An array of regexp pattern strings used to skip coverage collection 32 | // coveragePathIgnorePatterns: [ 33 | // "/node_modules/" 34 | // ], 35 | 36 | // A list of reporter names that Jest uses when writing coverage reports 37 | // coverageReporters: [ 38 | // "json", 39 | // "text", 40 | // "lcov", 41 | // "clover" 42 | // ], 43 | 44 | // An object that configures minimum threshold enforcement for coverage results 45 | // coverageThreshold: null, 46 | 47 | // Make calling deprecated APIs throw helpful error messages 48 | // errorOnDeprecated: false, 49 | 50 | // Force coverage collection from ignored files usin a array of glob patterns 51 | // forceCoverageMatch: [], 52 | 53 | // A path to a module which exports an async function that is triggered once before all test suites 54 | // globalSetup: null, 55 | 56 | // A path to a module which exports an async function that is triggered once after all test suites 57 | // globalTeardown: null, 58 | 59 | // A set of global variables that need to be available in all test environments 60 | // globals: {}, 61 | 62 | // An array of directory names to be searched recursively up from the requiring module's location 63 | // moduleDirectories: [ 64 | // "node_modules" 65 | // ], 66 | 67 | // An array of file extensions your modules use 68 | // moduleFileExtensions: [ 69 | // "js", 70 | // "json", 71 | // "jsx", 72 | // "node" 73 | // ], 74 | 75 | // A map from regular expressions to module names that allow to stub out resources with a single module 76 | // moduleNameMapper: {}, 77 | 78 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 79 | // modulePathIgnorePatterns: [], 80 | 81 | // Activates notifications for test results 82 | // notify: false, 83 | 84 | // An enum that specifies notification mode. Requires { notify: true } 85 | // notifyMode: "always", 86 | 87 | // A preset that is used as a base for Jest's configuration 88 | // preset: null, 89 | 90 | // Run tests from one or more projects 91 | // projects: null, 92 | 93 | // Use this configuration option to add custom reporters to Jest 94 | // reporters: undefined, 95 | 96 | // Automatically reset mock state between every test 97 | // resetMocks: false, 98 | 99 | // Reset the module registry before running each individual test 100 | // resetModules: false, 101 | 102 | // A path to a custom resolver 103 | // resolver: null, 104 | 105 | // Automatically restore mock state between every test 106 | // restoreMocks: false, 107 | 108 | // The root directory that Jest should scan for tests and modules within 109 | // rootDir: null, 110 | 111 | // A list of paths to directories that Jest should use to search for files in 112 | // roots: [ 113 | // "" 114 | // ], 115 | 116 | // Allows you to use a custom runner instead of Jest's default test runner 117 | // runner: "jest-runner", 118 | 119 | // The paths to modules that run some code to configure or set up the testing environment before each test 120 | // setupFiles: [], 121 | 122 | // The path to a module that runs some code to configure or set up the testing framework before each test 123 | // setupTestFrameworkScriptFile: null, 124 | 125 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 126 | // snapshotSerializers: [], 127 | 128 | // The test environment that will be used for testing 129 | testEnvironment: "jsdom", 130 | 131 | // Options that will be passed to the testEnvironment 132 | // testEnvironmentOptions: {}, 133 | 134 | // Adds a location field to test results 135 | // testLocationInResults: false, 136 | 137 | // The glob patterns Jest uses to detect test files 138 | testMatch: ["**/?(*.)+(spec|test).js?(x)"], 139 | 140 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 141 | testPathIgnorePatterns: ["/node_modules/", "webpack.config.test.js"], 142 | 143 | // The regexp pattern Jest uses to detect test files 144 | // testRegex: "", 145 | 146 | // This option allows the use of a custom results processor 147 | // testResultsProcessor: null, 148 | 149 | // This option allows use of a custom test runner 150 | // testRunner: "jasmine2", 151 | 152 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 153 | // testURL: "http://localhost", 154 | 155 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 156 | // timers: "real", 157 | 158 | // A map from regular expressions to paths to transformers 159 | // transform: null, 160 | 161 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 162 | // transformIgnorePatterns: [ 163 | // "/node_modules/" 164 | // ], 165 | 166 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 167 | // unmockedModulePathPatterns: undefined, 168 | 169 | // Indicates whether each individual test should be reported during the run 170 | // verbose: null, 171 | 172 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 173 | // watchPathIgnorePatterns: [], 174 | 175 | // Whether to use watchman for file crawling 176 | // watchman: true, 177 | }; 178 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-google-recaptcha", 3 | "version": "3.1.0", 4 | "description": "React Component Wrapper for Google reCAPTCHA", 5 | "main": "lib/index.js", 6 | "module": "lib/esm/index.js", 7 | "directories": { 8 | "lib": "lib/" 9 | }, 10 | "scripts": { 11 | "build": "rm -rf lib && npm run build:cjs && npm run build:esm", 12 | "build:cjs": "babel src --out-dir lib", 13 | "build:esm": "cross-env BABEL_ENV=esm babel src --out-dir lib/esm", 14 | "prepare": "npm run build", 15 | "pretty": "prettier --write src/*.js ./*.js test/*.js", 16 | "lint": "eslint ./", 17 | "lint:fix": "eslint ./ --fix", 18 | "test": "cross-env BABEL_ENV=development jest" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/dozoisch/react-google-recaptcha.git" 23 | }, 24 | "keywords": [ 25 | "react", 26 | "react-component", 27 | "captcha", 28 | "recaptcha", 29 | "google-recaptcha" 30 | ], 31 | "author": "Hugo Dozois ", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/dozoisch/react-google-recaptcha/issues" 35 | }, 36 | "homepage": "https://github.com/dozoisch/react-google-recaptcha", 37 | "peerDependencies": { 38 | "react": ">=16.4.1" 39 | }, 40 | "devDependencies": { 41 | "@babel/cli": "^7.19.3", 42 | "@babel/core": "^7.19.6", 43 | "@babel/preset-env": "^7.19.4", 44 | "@babel/preset-react": "^7.18.6", 45 | "@testing-library/react": "^13.4.0", 46 | "cross-env": "^7.0.3", 47 | "eslint": "^8.26.0", 48 | "eslint-config-prettier": "^8.5.0", 49 | "eslint-plugin-prettier": "^4.2.1", 50 | "eslint-plugin-react": "^7.31.10", 51 | "jest": "^29.2.2", 52 | "jest-environment-jsdom": "^29.2.2", 53 | "prettier": "^2.7.1", 54 | "react": "^18.2.0", 55 | "react-dom": "^18.2.0" 56 | }, 57 | "dependencies": { 58 | "prop-types": "^15.5.0", 59 | "react-async-script": "^1.2.0" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import RecaptchaWrapper from "./recaptcha-wrapper"; 2 | import ReCAPTCHA from "./recaptcha"; 3 | 4 | export default RecaptchaWrapper; 5 | export { ReCAPTCHA }; 6 | -------------------------------------------------------------------------------- /src/recaptcha-wrapper.js: -------------------------------------------------------------------------------- 1 | import ReCAPTCHA from "./recaptcha"; 2 | import makeAsyncScriptLoader from "react-async-script"; 3 | 4 | const callbackName = "onloadcallback"; 5 | const globalName = "grecaptcha"; 6 | 7 | function getOptions() { 8 | return (typeof window !== "undefined" && window.recaptchaOptions) || {}; 9 | } 10 | 11 | function getURL() { 12 | const dynamicOptions = getOptions(); 13 | const hostname = dynamicOptions.useRecaptchaNet ? "recaptcha.net" : "www.google.com"; 14 | if (dynamicOptions.enterprise) { 15 | return `https://${hostname}/recaptcha/enterprise.js?onload=${callbackName}&render=explicit`; 16 | } 17 | return `https://${hostname}/recaptcha/api.js?onload=${callbackName}&render=explicit`; 18 | } 19 | 20 | export default makeAsyncScriptLoader(getURL, { 21 | callbackName, 22 | globalName, 23 | attributes: getOptions().nonce ? { nonce: getOptions().nonce } : {}, 24 | })(ReCAPTCHA); 25 | -------------------------------------------------------------------------------- /src/recaptcha.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | export default class ReCAPTCHA extends React.Component { 5 | constructor() { 6 | super(); 7 | this.handleExpired = this.handleExpired.bind(this); 8 | this.handleErrored = this.handleErrored.bind(this); 9 | this.handleChange = this.handleChange.bind(this); 10 | this.handleRecaptchaRef = this.handleRecaptchaRef.bind(this); 11 | } 12 | 13 | getCaptchaFunction(fnName) { 14 | if (this.props.grecaptcha) { 15 | if (this.props.grecaptcha.enterprise) { 16 | return this.props.grecaptcha.enterprise[fnName]; 17 | } 18 | return this.props.grecaptcha[fnName]; 19 | } 20 | return null; 21 | } 22 | 23 | getValue() { 24 | const getResponse = this.getCaptchaFunction("getResponse"); 25 | if (getResponse && this._widgetId !== undefined) { 26 | return getResponse(this._widgetId); 27 | } 28 | return null; 29 | } 30 | 31 | getWidgetId() { 32 | if (this.props.grecaptcha && this._widgetId !== undefined) { 33 | return this._widgetId; 34 | } 35 | return null; 36 | } 37 | 38 | execute() { 39 | const execute = this.getCaptchaFunction("execute"); 40 | if (execute && this._widgetId !== undefined) { 41 | return execute(this._widgetId); 42 | } else { 43 | this._executeRequested = true; 44 | } 45 | } 46 | 47 | executeAsync() { 48 | return new Promise((resolve, reject) => { 49 | this.executionResolve = resolve; 50 | this.executionReject = reject; 51 | this.execute(); 52 | }); 53 | } 54 | 55 | reset() { 56 | const resetter = this.getCaptchaFunction("reset"); 57 | if (resetter && this._widgetId !== undefined) { 58 | resetter(this._widgetId); 59 | } 60 | } 61 | 62 | forceReset() { 63 | const resetter = this.getCaptchaFunction("reset"); 64 | if (resetter) { 65 | resetter(); 66 | } 67 | } 68 | 69 | handleExpired() { 70 | if (this.props.onExpired) { 71 | this.props.onExpired(); 72 | } else { 73 | this.handleChange(null); 74 | } 75 | } 76 | 77 | handleErrored() { 78 | if (this.props.onErrored) { 79 | this.props.onErrored(); 80 | } 81 | if (this.executionReject) { 82 | this.executionReject(); 83 | delete this.executionResolve; 84 | delete this.executionReject; 85 | } 86 | } 87 | 88 | handleChange(token) { 89 | if (this.props.onChange) { 90 | this.props.onChange(token); 91 | } 92 | if (this.executionResolve) { 93 | this.executionResolve(token); 94 | delete this.executionReject; 95 | delete this.executionResolve; 96 | } 97 | } 98 | 99 | explicitRender() { 100 | const render = this.getCaptchaFunction("render"); 101 | if (render && this._widgetId === undefined) { 102 | const wrapper = document.createElement("div"); 103 | this._widgetId = render(wrapper, { 104 | sitekey: this.props.sitekey, 105 | callback: this.handleChange, 106 | theme: this.props.theme, 107 | type: this.props.type, 108 | tabindex: this.props.tabindex, 109 | "expired-callback": this.handleExpired, 110 | "error-callback": this.handleErrored, 111 | size: this.props.size, 112 | stoken: this.props.stoken, 113 | hl: this.props.hl, 114 | badge: this.props.badge, 115 | isolated: this.props.isolated, 116 | }); 117 | this.captcha.appendChild(wrapper); 118 | } 119 | if (this._executeRequested && this.props.grecaptcha && this._widgetId !== undefined) { 120 | this._executeRequested = false; 121 | this.execute(); 122 | } 123 | } 124 | 125 | componentDidMount() { 126 | this.explicitRender(); 127 | } 128 | 129 | componentDidUpdate() { 130 | this.explicitRender(); 131 | } 132 | 133 | handleRecaptchaRef(elem) { 134 | this.captcha = elem; 135 | } 136 | 137 | render() { 138 | // consume properties owned by the reCATPCHA, pass the rest to the div so the user can style it. 139 | /* eslint-disable no-unused-vars */ 140 | const { 141 | sitekey, 142 | onChange, 143 | theme, 144 | type, 145 | tabindex, 146 | onExpired, 147 | onErrored, 148 | size, 149 | stoken, 150 | grecaptcha, 151 | badge, 152 | hl, 153 | isolated, 154 | ...childProps 155 | } = this.props; 156 | /* eslint-enable no-unused-vars */ 157 | return
; 158 | } 159 | } 160 | 161 | ReCAPTCHA.displayName = "ReCAPTCHA"; 162 | ReCAPTCHA.propTypes = { 163 | sitekey: PropTypes.string.isRequired, 164 | onChange: PropTypes.func, 165 | grecaptcha: PropTypes.object, 166 | theme: PropTypes.oneOf(["dark", "light"]), 167 | type: PropTypes.oneOf(["image", "audio"]), 168 | tabindex: PropTypes.number, 169 | onExpired: PropTypes.func, 170 | onErrored: PropTypes.func, 171 | size: PropTypes.oneOf(["compact", "normal", "invisible"]), 172 | stoken: PropTypes.string, 173 | hl: PropTypes.string, 174 | badge: PropTypes.oneOf(["bottomright", "bottomleft", "inline"]), 175 | isolated: PropTypes.bool, 176 | }; 177 | ReCAPTCHA.defaultProps = { 178 | onChange: () => {}, 179 | theme: "light", 180 | type: "image", 181 | tabindex: 0, 182 | size: "normal", 183 | badge: "bottomright", 184 | }; 185 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true, 4 | "node": true 5 | }, 6 | "rules": { 7 | "no-script-url": 1, 8 | "no-unused-expressions": 0, 9 | "react/no-multi-comp": 0 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import "es5-shim"; 2 | import "es6-shim"; 3 | const testsContext = require.context(".", true, /-spec$/); 4 | testsContext.keys().forEach(testsContext); 5 | -------------------------------------------------------------------------------- /test/recaptcha-wrapper.spec.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render } from "@testing-library/react"; 3 | import ReCAPTCHA from "../src/recaptcha-wrapper"; 4 | 5 | const VALUE = "some value"; 6 | const WIDGET_ID = "someWidgetId"; 7 | 8 | const grecaptchaMock = { 9 | render(node, options) { 10 | expect(node).toBeTruthy(); 11 | expect(options).toBeTruthy(); 12 | return WIDGET_ID; 13 | }, 14 | 15 | getResponse(widgetId) { 16 | expect(widgetId).toBe(WIDGET_ID); 17 | return VALUE; 18 | }, 19 | }; 20 | 21 | describe("ReCAPTCHAWrapper", () => { 22 | beforeEach(() => { 23 | window.grecaptcha = grecaptchaMock; 24 | }); 25 | afterEach(() => { 26 | delete window.grecaptcha; 27 | }); 28 | it("should be wrapped properly", () => { 29 | expect(ReCAPTCHA.displayName).toBe("AsyncScriptLoader(ReCAPTCHA)"); 30 | }); 31 | it("should proxy functions", () => { 32 | const ReCaptchaRef = React.createRef(); 33 | 34 | render(); 35 | expect(ReCaptchaRef.current.getValue()).toBe(VALUE); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/recaptcha.spec.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render } from "@testing-library/react"; 3 | 4 | import ReCAPTCHA from "../src/recaptcha"; // eslint-disable-line no-unused-vars 5 | 6 | describe("ReCAPTCHA", () => { 7 | it("Rendered Component should be a div", () => { 8 | const { container } = render(); 9 | expect(container.firstChild.nodeName).toBe("DIV"); 10 | }); 11 | it("Rendered Component should contained passed props", () => { 12 | const props = { 13 | className: "TheClassName", 14 | id: "superdefinedId", 15 | onChange: jest.fn(), 16 | }; 17 | const { container } = render(); 18 | expect(container.firstChild.id).toBe(props.id); 19 | expect(container.firstChild.className).toBe(props.className); 20 | }); 21 | 22 | it("should call grecaptcha.render, when it is already loaded", () => { 23 | return new Promise((resolve) => { 24 | const grecaptchaMock = { 25 | render(node, options) { 26 | expect(node).toBeTruthy(); 27 | expect(options.sitekey).toBe("xxx"); 28 | resolve(); 29 | }, 30 | }; 31 | render(); 32 | }); 33 | }); 34 | it("reset, should call grecaptcha.reset with the widget id", () => { 35 | const WIDGET_ID = "someWidgetId"; 36 | const grecaptchaMock = { 37 | render() { 38 | return WIDGET_ID; 39 | }, 40 | reset: jest.fn(), 41 | }; 42 | const ReCaptchaRef = React.createRef(); 43 | render( 44 | , 50 | ); 51 | ReCaptchaRef.current.reset(); 52 | expect(grecaptchaMock.reset).toBeCalledWith(WIDGET_ID); 53 | }); 54 | it("execute, should call grecaptcha.execute with the widget id", () => { 55 | const WIDGET_ID = "someWidgetId"; 56 | const grecaptchaMock = { 57 | render() { 58 | return WIDGET_ID; 59 | }, 60 | execute: jest.fn(), 61 | }; 62 | // wrapping component example that applies a ref to ReCAPTCHA 63 | class WrappingComponent extends React.Component { 64 | constructor(props) { 65 | super(props); 66 | this._internalRef = React.createRef(); 67 | } 68 | render() { 69 | return ( 70 |
71 | 78 |
79 | ); 80 | } 81 | } 82 | const wrappingRef = React.createRef(); 83 | render(); 84 | wrappingRef.current._internalRef.current.execute(); 85 | expect(grecaptchaMock.execute).toBeCalledWith(WIDGET_ID); 86 | }); 87 | it("executeAsync, should call grecaptcha.execute with the widget id", () => { 88 | const WIDGET_ID = "someWidgetId"; 89 | const grecaptchaMock = { 90 | render() { 91 | return WIDGET_ID; 92 | }, 93 | execute: jest.fn(), 94 | }; 95 | // wrapping component example that applies a ref to ReCAPTCHA 96 | class WrappingComponent extends React.Component { 97 | constructor(props) { 98 | super(props); 99 | this._internalRef = React.createRef(); 100 | } 101 | render() { 102 | return ( 103 |
104 | 111 |
112 | ); 113 | } 114 | } 115 | const wrappingRef = React.createRef(); 116 | render(); 117 | wrappingRef.current._internalRef.current.executeAsync(); 118 | expect(grecaptchaMock.execute).toBeCalledWith(WIDGET_ID); 119 | }); 120 | it("executeAsync, should return a promise that resolves with the token", () => { 121 | const WIDGET_ID = "someWidgetId"; 122 | const TOKEN = "someToken"; 123 | const grecaptchaMock = (() => { 124 | let _callback; 125 | return { 126 | render(_, { callback }) { 127 | _callback = callback; 128 | return WIDGET_ID; 129 | }, 130 | execute() { 131 | _callback(TOKEN); 132 | }, 133 | }; 134 | })(); 135 | // wrapping component example that applies a ref to ReCAPTCHA 136 | class WrappingComponent extends React.Component { 137 | constructor(props) { 138 | super(props); 139 | this._internalRef = React.createRef(); 140 | } 141 | render() { 142 | return ( 143 |
144 | 151 |
152 | ); 153 | } 154 | } 155 | 156 | const wrappingRef = React.createRef(); 157 | render(); 158 | const executeAsyncDirectValue = wrappingRef.current._internalRef.current.executeAsync(); 159 | expect(executeAsyncDirectValue).toBeInstanceOf(Promise); 160 | return executeAsyncDirectValue.then((executeAsyncResolveValue) => { 161 | expect(executeAsyncResolveValue).toBe(TOKEN); 162 | }); 163 | }); 164 | describe("Expired", () => { 165 | it("should call onChange with null when response is expired", () => { 166 | const WIDGET_ID = "someWidgetId"; 167 | const onChange = jest.fn(); 168 | const grecaptchaMock = { 169 | render() { 170 | return WIDGET_ID; 171 | }, 172 | }; 173 | const ReCaptchaRef = React.createRef(); 174 | render( 175 | , 181 | ); 182 | ReCaptchaRef.current.handleExpired(); 183 | expect(onChange).toBeCalledWith(null); 184 | }); 185 | it("should call onExpired when response is expired", () => { 186 | const WIDGET_ID = "someWidgetId"; 187 | const onChange = jest.fn(); 188 | const onExpired = jest.fn(); 189 | const grecaptchaMock = { 190 | render() { 191 | return WIDGET_ID; 192 | }, 193 | }; 194 | const ReCaptchaRef = React.createRef(); 195 | render( 196 | , 203 | ); 204 | ReCaptchaRef.current.handleExpired(); 205 | expect(onChange).not.toHaveBeenCalled(); 206 | expect(onExpired).toHaveBeenCalled(); 207 | }); 208 | }); 209 | describe("Errored", () => { 210 | it("should call onErrored when grecaptcha errored", () => { 211 | const WIDGET_ID = "someWidgetId"; 212 | const onErrored = jest.fn(); 213 | const grecaptchaMock = { 214 | render() { 215 | return WIDGET_ID; 216 | }, 217 | }; 218 | const ReCaptchaRef = React.createRef(); 219 | render( 220 | , 227 | ); 228 | ReCaptchaRef.current.handleErrored(); 229 | expect(onErrored).toHaveBeenCalled(); 230 | }); 231 | }); 232 | }); 233 | --------------------------------------------------------------------------------