├── .gitignore ├── .npmignore ├── .nvmrc ├── .prettierrc ├── LICENCE.md ├── README.md ├── __tests__ ├── google-recaptcha-provider.test.tsx ├── tsconfig.json ├── use-google-recaptcha.test.tsx └── with-google-recaptcha.test.tsx ├── example ├── google-recaptcha-example.tsx ├── index.tsx └── with-google-recaptcha-example.tsx ├── index.html ├── jest.config.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── google-recaptcha-provider.tsx ├── google-recaptcha.tsx ├── index.ts ├── use-google-recaptcha.tsx ├── utils.ts └── with-google-recaptcha.tsx ├── tsconfig.json ├── tslint.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node 3 | # Edit at https://www.gitignore.io/?templates=node 4 | 5 | ### Node ### 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 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 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # Optional npm cache directory 48 | .npm 49 | 50 | # Optional eslint cache 51 | .eslintcache 52 | 53 | # Optional REPL history 54 | .node_repl_history 55 | 56 | # Output of 'npm pack' 57 | *.tgz 58 | 59 | # Yarn Integrity file 60 | .yarn-integrity 61 | 62 | # dotenv environment variables file 63 | .env 64 | 65 | # parcel-bundler cache (https://parceljs.org/) 66 | .cache 67 | 68 | # next.js build output 69 | .next 70 | 71 | # nuxt.js build output 72 | .nuxt 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless/ 79 | 80 | # FuseBox cache 81 | .fusebox/ 82 | 83 | #DynamoDB Local files 84 | .dynamodb/ 85 | 86 | # End of https://www.gitignore.io/api/node 87 | 88 | dist 89 | build 90 | .rpt2_cache 91 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | example 2 | tslint.json 3 | webpack.config.js 4 | index.html 5 | __tests__ 6 | jest.config.js 7 | .prettierrc 8 | build 9 | src 10 | .nvmrc 11 | rollup.config.js 12 | tsconfig.json -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v14.8.0 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "none", 4 | "tabWidth": 2, 5 | "arrowParens": "avoid" 6 | } 7 | -------------------------------------------------------------------------------- /LICENCE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Duong Tran 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

React Google Recaptcha V3

2 |
3 | 4 | [React](https://reactjs.org/) library for integrating Google ReCaptcha V3 to your App. 5 | 6 | [![npm package](https://img.shields.io/npm/v/react-google-recaptcha-v3/latest.svg)](https://www.npmjs.com/package/react-google-recaptcha-v3) 7 | ![Code style](https://img.shields.io/badge/code_style-prettier-ff69b4.svg) 8 | ![type definition](https://img.shields.io/npm/types/react-google-recaptcha-v3) 9 | 10 |
11 | 12 | ## Install 13 | 14 | ```bash 15 | npm install react-google-recaptcha-v3 16 | ``` 17 | 18 | ## Usage 19 | 20 | #### Provide Recaptcha Key 21 | 22 | To use `react-google-recaptcha-v3`, you need to create a recaptcha key for your domain, you can get one from [here](https://www.google.com/recaptcha/intro/v3.html). 23 | 24 | #### Enterprise 25 | 26 | When you enable to use the enterprise version, **you must create new keys**. These keys will replace any Site Keys you created in reCAPTCHA. Check the [migration guide](https://cloud.google.com/recaptcha-enterprise/docs/migrate-recaptcha). 27 | 28 | To work properly, you **must** select the Integration type to be `Scoring` since is equivalent to the reCAPTCHA v3. 29 | 30 | The complete documentation to the enterprise version you can see [here](https://cloud.google.com/recaptcha-enterprise/docs/quickstart). 31 | 32 | #### Components 33 | 34 | ##### GoogleReCaptchaProvider 35 | 36 | `react-google-recaptcha-v3` provides a `GoogleReCaptchaProvider` provider component that should be used to wrap around your components. 37 | 38 | `GoogleReCaptchaProvider`'s responsibility is to load the necessary reCaptcha script and provide access to reCaptcha to the rest of your application. 39 | 40 | Usually, your application only needs one provider. You should place it as high as possible in your React tree. It's to make sure you only have one instance of Google Recaptcha per page and it doesn't reload unecessarily when your components re-rendered. 41 | 42 | Same thing applied when you use this library with framework such as Next.js or React Router and only want to include the script on a single page. Try to make sure you only have one instance of the provider on a React tree and to place it as high (on the tree) as possible. 43 | 44 | | **Props** | **Type** | **Default** | **Required?** | **Note** | 45 | |----------------------|:----------------:| ----------: | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 46 | | reCaptchaKey | String | | Yes | Your recaptcha key, get one from [here](https://www.google.com/recaptcha/intro/v3.html) | 47 | | scriptProps | Object | | No | You can customize the injected `script` tag with this prop. It allows you to add `async`, `defer`, `nonce` attributes to the script tag. You can also control whether the injected script will be added to the document body or head with `appendTo` attribute. | 48 | | language | String | | No | optional prop to support different languages that is supported by Google Recaptcha. https://developers.google.com/recaptcha/docs/language | 49 | | useRecaptchaNet | Boolean | false | No | The provider also provide the prop `useRecaptchaNet` to load script from `recaptcha.net`: https://developers.google.com/recaptcha/docs/faq#can-i-use-recaptcha-globally | 50 | | useEnterprise | Boolean | false | No | [Enterprise option](#enterprise) | 51 | | container.element | String HTMLElement | | No | Container ID where the recaptcha badge will be rendered | 52 | | container.parameters | Object | | No | Configuration for the inline badge (See google recaptcha docs) | 53 | 54 | ```javascript 55 | import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3'; 56 | 57 | ReactDom.render( 58 | 77 | 78 | , 79 | document.getElementById('app') 80 | ); 81 | ``` 82 | 83 | There are three ways to trigger the recaptcha validation: using the `GoogleReCaptcha` component, wrapping your component with the HOC `withGoogleReCaptcha`, or using the custom hook `useGoogleReCaptcha`. 84 | 85 | #### GoogleReCaptcha 86 | 87 | `GoogleRecaptcha` is a react component that can be used in your app to trigger the validation. It provides a prop `onVerify`, which will be called once the verify is done successfully, also supports a prop `refreshReCaptcha` which supports any type of value and is used to force recaptcha to revalidate (you can use a timestamp updated after every submit), there is an example below. 88 | 89 | ```javascript 90 | import { 91 | GoogleReCaptchaProvider, 92 | GoogleReCaptcha 93 | } from 'react-google-recaptcha-v3'; 94 | 95 | ReactDom.render( 96 | 97 | 98 | , 99 | document.getElementById('app') 100 | ); 101 | ``` 102 | 103 | ```javascript 104 | // IMPORTANT NOTES: The `GoogleReCaptcha` component is a wrapper around `useGoogleRecaptcha` hook and use `useEffect` to run the verification. 105 | // It's important that you understand how React hooks work to use it properly. 106 | // Avoid using inline function for the `onVerify` props as it can possibly cause the verify function to run continously. 107 | // To avoid that problem, you can use a memoized function provided by `React.useCallback` or a class method 108 | // The code below is an example that inline function can result in an infinite loop and the verify function runs continously: 109 | 110 | const MyComponent: FC = () => { 111 | const [token, setToken] = useState(); 112 | 113 | return ( 114 |
115 | { 117 | setToken(token); 118 | }} 119 | /> 120 |
121 | ); 122 | }; 123 | ``` 124 | 125 | ```javascript 126 | // Example of refreshReCaptcha option: 127 | 128 | const MyComponent: FC = () => { 129 | const [token, setToken] = useState(); 130 | const [refreshReCaptcha, setRefreshReCaptcha] = useState(false); 131 | 132 | const onVerify = useCallback((token) => { 133 | setToken(token); 134 | }); 135 | 136 | const doSomething = () => { 137 | /* do something like submit a form and then refresh recaptcha */ 138 | setRefreshReCaptcha(r => !r); 139 | } 140 | 141 | return ( 142 |
143 | 147 | 150 |
151 | ); 152 | }; 153 | ``` 154 | 155 | #### React Hook: useGoogleReCaptcha (recommended approach) 156 | 157 | If you prefer a React Hook approach over the old good Higher Order Component, you can choose to use the custom hook `useGoogleReCaptcha` over the HOC `withGoogleReCaptcha`. 158 | 159 | The `executeRecaptcha` function returned from the hook can be undefined when the recaptcha script has not been successfully loaded. 160 | You can do a null check to see if it's available or not. 161 | 162 | How to use the hook: 163 | 164 | ```javascript 165 | import { 166 | GoogleReCaptchaProvider, 167 | useGoogleReCaptcha 168 | } from 'react-google-recaptcha-v3'; 169 | 170 | const YourReCaptchaComponent = () => { 171 | const { executeRecaptcha } = useGoogleReCaptcha(); 172 | 173 | // Create an event handler so you can call the verification on button click event or form submit 174 | const handleReCaptchaVerify = useCallback(async () => { 175 | if (!executeRecaptcha) { 176 | console.log('Execute recaptcha not yet available'); 177 | return; 178 | } 179 | 180 | const token = await executeRecaptcha('yourAction'); 181 | // Do whatever you want with the token 182 | }, [executeRecaptcha]); 183 | 184 | // You can use useEffect to trigger the verification as soon as the component being loaded 185 | useEffect(() => { 186 | handleReCaptchaVerify(); 187 | }, [handleReCaptchaVerify]); 188 | 189 | return ; 190 | }; 191 | 192 | ReactDom.render( 193 | 194 | 195 | , 196 | document.getElementById('app') 197 | ); 198 | ``` 199 | 200 | #### withGoogleReCaptcha 201 | 202 | `GoogleRecaptcha` is a HOC (higher order component) that can be used to integrate reCaptcha validation with your component and trigger the validation programmatically. It inject the wrapped component with `googleReCaptchaProps` object. 203 | 204 | The object contains the `executeRecaptcha` function that can be called to validate the user action. 205 | 206 | You are recommended to use the custom hook `useGoogleReCaptcha` over the HOC whenever you can. The HOC can be removed in future version. 207 | 208 | ```javascript 209 | import { 210 | GoogleReCaptchaProvider, 211 | withGoogleReCaptcha 212 | } from 'react-google-recaptcha-v3'; 213 | 214 | class ReCaptchaComponent extends Component<{}> { 215 | handleVerifyRecaptcha = async () => { 216 | const { executeRecaptcha } = (this.props as IWithGoogleReCaptchaProps) 217 | .googleReCaptchaProps; 218 | 219 | if (!executeRecaptcha) { 220 | console.log('Recaptcha has not been loaded'); 221 | 222 | return; 223 | } 224 | 225 | const token = await executeRecaptcha('homepage'); 226 | }; 227 | 228 | render() { 229 | return ( 230 |
231 | 232 |
233 | ); 234 | } 235 | } 236 | 237 | export const WithGoogleRecaptchaExample = 238 | withGoogleReCaptcha(ReCaptchaComponent); 239 | 240 | ReactDom.render( 241 | 242 | 243 | , 244 | document.getElementById('app') 245 | ); 246 | ``` 247 | 248 | ## Example 249 | 250 | An example of how to use these two hooks can found inside the `example` folder. You will need to provide an .env file if you want to run it on your own machine. 251 | 252 | ``` 253 | RECAPTCHA_KEY=[YOUR_RECAPTCHA_KEY] 254 | ``` 255 | -------------------------------------------------------------------------------- /__tests__/google-recaptcha-provider.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { GoogleReCaptchaProvider } from 'src/google-recaptcha-provider'; 3 | import { render, waitFor } from '@testing-library/react'; 4 | 5 | describe('', () => { 6 | it('inject google recaptcha script to the document', () => { 7 | render( 8 | 9 |
10 | 11 | ); 12 | 13 | const scriptElm = document.querySelector('#google-recaptcha-v3'); 14 | expect(scriptElm).not.toBeNull(); 15 | }); 16 | 17 | it('remove google recaptcha script from the document when being unmounted', async () => { 18 | const { unmount } = render( 19 | 20 |
21 | 22 | ); 23 | 24 | const scriptElm = document.querySelector('#google-recaptcha-v3'); 25 | expect(scriptElm).not.toBeNull(); 26 | 27 | unmount(); 28 | 29 | await waitFor(() => { 30 | const scriptElm = document.querySelector('#google-recaptcha-v3'); 31 | expect(scriptElm).toBeNull(); 32 | }); 33 | }); 34 | 35 | it('accept a useRecaptchaNet prop to load recaptcha from recaptcha.net', () => { 36 | render( 37 | 38 |
39 | 40 | ); 41 | 42 | const scriptElm = document.querySelector('#google-recaptcha-v3'); 43 | 44 | expect(scriptElm!.getAttribute('src')).toEqual( 45 | 'https://www.recaptcha.net/recaptcha/api.js?render=TESTKEY' 46 | ); 47 | }); 48 | 49 | it('puts a nonce to the script if provided', () => { 50 | render( 51 | 55 |
56 | 57 | ); 58 | 59 | const scriptElm = document.getElementById('google-recaptcha-v3'); 60 | 61 | expect(scriptElm!.getAttribute('nonce')).toEqual('NONCE'); 62 | }); 63 | 64 | it('puts a defer to the script if provided', () => { 65 | render( 66 | 73 |
74 | 75 | ); 76 | 77 | const scriptElm = document.getElementById('google-recaptcha-v3'); 78 | 79 | expect(scriptElm!.getAttribute('defer')).toEqual(''); 80 | }); 81 | 82 | it('does not reload script if scriptProps object stays the same', async () => { 83 | const { rerender } = render( 84 | 85 |
86 | 87 | ); 88 | 89 | const scriptElm = document.querySelector('#google-recaptcha-v3'); 90 | expect(scriptElm).not.toBeNull(); 91 | 92 | rerender( 93 | 94 |
95 | 96 | ); 97 | 98 | expect(scriptElm).toBe(document.querySelector('#google-recaptcha-v3')); 99 | }); 100 | 101 | it('reloads script on scriptProps changes', async () => { 102 | const { rerender } = render( 103 | 104 |
105 | 106 | ); 107 | 108 | const scriptElm = document.querySelector('#google-recaptcha-v3'); 109 | expect(scriptElm).not.toBeNull(); 110 | 111 | rerender( 112 | 113 |
114 | 115 | ); 116 | 117 | expect(scriptElm).not.toBe(document.querySelector('#google-recaptcha-v3')); 118 | }); 119 | 120 | describe('when using enterprise version', () => { 121 | it('accept an enterprise prop to load recaptcha from enterprise source', () => { 122 | render( 123 | 124 |
125 | 126 | ); 127 | 128 | const scriptElm = document.getElementById('google-recaptcha-v3'); 129 | 130 | expect(scriptElm!.getAttribute('src')).toEqual( 131 | 'https://www.google.com/recaptcha/enterprise.js?render=TESTKEY' 132 | ); 133 | }); 134 | 135 | it('should load recaptcha from recaptcha.net', () => { 136 | render( 137 | 142 |
143 | 144 | ); 145 | 146 | const scriptElm = document.getElementById('google-recaptcha-v3'); 147 | 148 | expect(scriptElm!.getAttribute('src')).toEqual( 149 | 'https://www.recaptcha.net/recaptcha/enterprise.js?render=TESTKEY' 150 | ); 151 | }); 152 | }); 153 | }); 154 | -------------------------------------------------------------------------------- /__tests__/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "ES2015", 5 | "lib": ["es2015", "es7", "dom"], 6 | "jsx": "react", 7 | "declaration": true, 8 | "declarationMap": true, 9 | "declarationDir": "./dist/types", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "strict": true, 13 | "noUnusedLocals": true, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "moduleResolution": "node", 17 | "baseUrl": "..", 18 | "experimentalDecorators": true, 19 | "emitDecoratorMetadata": true, 20 | "esModuleInterop": true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /__tests__/use-google-recaptcha.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { GoogleReCaptchaProvider } from 'src/google-recaptcha-provider'; 3 | import { useGoogleReCaptcha } from 'src/use-google-recaptcha'; 4 | import { renderHook } from '@testing-library/react-hooks'; 5 | 6 | const TestWrapper: React.FC = ({ children }) => ( 7 | 8 | {children} 9 | 10 | ); 11 | 12 | describe('useGoogleReCaptcha hook', () => { 13 | it('return google recaptcha context value', () => { 14 | const { result } = renderHook(() => useGoogleReCaptcha(), { 15 | wrapper: TestWrapper 16 | }); 17 | 18 | expect(result.current).toHaveProperty('executeRecaptcha'); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /__tests__/with-google-recaptcha.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | import * as React from 'react'; 3 | import { 4 | GoogleReCaptchaProvider, 5 | IGoogleReCaptchaConsumerProps 6 | } from 'src/google-recaptcha-provider'; 7 | import { 8 | IWithGoogleReCaptchaProps, 9 | withGoogleReCaptcha 10 | } from 'src/with-google-recaptcha'; 11 | 12 | const TestComponent = ({ 13 | googleReCaptchaProps, 14 | onLoad 15 | }: Partial & { 16 | onLoad: (props: IGoogleReCaptchaConsumerProps) => void; 17 | }) => { 18 | React.useEffect(() => { 19 | if (!googleReCaptchaProps) { 20 | return; 21 | } 22 | 23 | console.log(googleReCaptchaProps); 24 | 25 | onLoad(googleReCaptchaProps); 26 | }, [googleReCaptchaProps]); 27 | 28 | return
; 29 | }; 30 | 31 | const WrappedTestComponent = withGoogleReCaptcha(TestComponent); 32 | 33 | const TestProvider = ({ 34 | onLoad 35 | }: { 36 | onLoad: (props: IGoogleReCaptchaConsumerProps) => void; 37 | }) => ( 38 | 39 | 40 | 41 | ); 42 | 43 | describe('withGoogleRecaptcha HOC', () => { 44 | it('inject the wrapped component with googleReCaptcha prop', () => { 45 | const testFn = jest.fn(); 46 | 47 | render(); 48 | 49 | expect(testFn).toBeCalledWith( 50 | expect.objectContaining({ executeRecaptcha: undefined }) 51 | ); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /example/google-recaptcha-example.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, FC, useCallback, useEffect } from 'react'; 2 | import { useGoogleReCaptcha } from '../src/use-google-recaptcha'; 3 | 4 | export const GoogleRecaptchaExample: FC = () => { 5 | const { executeRecaptcha } = useGoogleReCaptcha(); 6 | const [token, setToken] = useState(''); 7 | const [noOfVerifications, setNoOfVerifications] = useState(0); 8 | const [dynamicAction, setDynamicAction] = useState('homepage'); 9 | const [actionToChange, setActionToChange] = useState(''); 10 | 11 | const clickHandler = useCallback(async () => { 12 | if (!executeRecaptcha) { 13 | return; 14 | } 15 | 16 | const result = await executeRecaptcha(dynamicAction); 17 | 18 | setToken(result); 19 | setNoOfVerifications(noOfVerifications => noOfVerifications + 1); 20 | }, [dynamicAction, executeRecaptcha]); 21 | 22 | const handleTextChange = useCallback(event => { 23 | setActionToChange(event.target.value); 24 | }, []); 25 | 26 | const handleCommitAction = useCallback(() => { 27 | setDynamicAction(actionToChange); 28 | }, [actionToChange]); 29 | 30 | useEffect(() => { 31 | if (!executeRecaptcha || !dynamicAction) { 32 | return; 33 | } 34 | 35 | const handleReCaptchaVerify = async () => { 36 | const token = await executeRecaptcha(dynamicAction); 37 | setToken(token); 38 | setNoOfVerifications(noOfVerifications => noOfVerifications + 1); 39 | }; 40 | 41 | handleReCaptchaVerify(); 42 | }, [executeRecaptcha, dynamicAction]); 43 | 44 | return ( 45 |
46 |
47 |

Current ReCaptcha action: {dynamicAction}

48 | 49 | 50 |
51 |
52 | 53 |
54 | {token &&

Token: {token}

} 55 |

No of verifications: {noOfVerifications}

56 |
57 | ); 58 | }; 59 | -------------------------------------------------------------------------------- /example/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDom from 'react-dom'; 3 | import { GoogleReCaptchaProvider } from '../src/google-recaptcha-provider'; 4 | import { GoogleRecaptchaExample } from './google-recaptcha-example'; 5 | import { WithGoogleRecaptchaExample } from './with-google-recaptcha-example'; 6 | 7 | ReactDom.render( 8 | 13 |

Google Recaptcha Example

14 | 15 | 16 |
, 17 | document.getElementById('app') 18 | ); 19 | -------------------------------------------------------------------------------- /example/with-google-recaptcha-example.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | IWithGoogleReCaptchaProps, 4 | withGoogleReCaptcha 5 | } from '../src/with-google-recaptcha'; 6 | 7 | class ReCaptchaComponent extends Component<{}, { token?: string }> { 8 | constructor(props: {}) { 9 | super(props); 10 | 11 | this.state = { token: undefined }; 12 | } 13 | 14 | handleVerifyRecaptcha = async () => { 15 | const { executeRecaptcha } = (this.props as IWithGoogleReCaptchaProps) 16 | .googleReCaptchaProps; 17 | 18 | if (!executeRecaptcha) { 19 | console.log('Recaptcha has not been loaded'); 20 | 21 | return; 22 | } 23 | 24 | const token = await executeRecaptcha('homepage'); 25 | 26 | this.setState({ token }); 27 | }; 28 | 29 | render() { 30 | const { token } = this.state; 31 | return ( 32 |
33 |

With Google Recaptcha HOC Example

34 | 35 |

Token: {token}

36 |
37 | ); 38 | } 39 | } 40 | 41 | export const WithGoogleRecaptchaExample = 42 | withGoogleReCaptcha(ReCaptchaComponent); 43 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | coverageDirectory: 'coverage', 4 | coveragePathIgnorePatterns: ['/node_modules/'], 5 | globals: { 6 | 'ts-jest': { 7 | tsConfig: '__tests__/tsconfig.json' 8 | } 9 | }, 10 | moduleFileExtensions: ['ts', 'tsx', 'js'], 11 | testEnvironment: 'node', 12 | testMatch: ['**/__tests__/*.test.+(ts|tsx|js)'], 13 | transform: { 14 | '^.+\\.tsx?$': 'ts-jest' 15 | }, 16 | moduleNameMapper: { 17 | '^src/(.*)$': '/src/$1' 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-google-recaptcha-v3", 3 | "version": "1.11.0", 4 | "description": "React component for google-recaptcha v3", 5 | "module": "dist/react-google-recaptcha-v3.esm.js", 6 | "main": "dist/react-google-recaptcha-v3.cjs.js", 7 | "types": "dist/types/index.d.ts", 8 | "scripts": { 9 | "test": "jest --verbose --env=jsdom", 10 | "build": "rollup -c rollup.config.js", 11 | "prepublishOnly": "npm run test && npm run build", 12 | "build-example": "webpack --config webpack.config.js", 13 | "lint": "tslint -c tslint.json 'src/**/*.ts'" 14 | }, 15 | "author": "Duong Tran", 16 | "homepage": "https://github.com/t49tran/react-google-recaptcha-v3", 17 | "license": "MIT", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/t49tran/react-google-recaptcha-v3" 21 | }, 22 | "peerDependencies": { 23 | "react": "^16.3 || ^17.0 || ^18.0 || ^19.0", 24 | "react-dom": "^17.0 || ^18.0 || ^19.0" 25 | }, 26 | "dependencies": { 27 | "hoist-non-react-statics": "^3.3.2" 28 | }, 29 | "devDependencies": { 30 | "@testing-library/react": "11.2.6", 31 | "@testing-library/react-hooks": "5.1.1", 32 | "@types/hoist-non-react-statics": "3.3.1", 33 | "@types/jest": "24.0.13", 34 | "@types/node": "14.0.1", 35 | "@types/react": "17.0.2", 36 | "@types/react-dom": "17.0.2", 37 | "awesome-typescript-loader": "5.2.1", 38 | "dotenv-webpack": "1.8.0", 39 | "jest": "26.6.3", 40 | "react": "17.0.2", 41 | "react-dom": "17.0.2", 42 | "rollup": "1.14.3", 43 | "rollup-plugin-commonjs": "10.0.0", 44 | "rollup-plugin-node-resolve": "5.0.1", 45 | "rollup-plugin-sourcemaps": "0.4.2", 46 | "rollup-plugin-terser": "7.0.2", 47 | "rollup-plugin-typescript2": "0.27.1", 48 | "source-map-loader": "0.2.4", 49 | "ts-jest": "26.5.4", 50 | "tslint": "5.15.0", 51 | "tslint-react": "4.0.0", 52 | "typescript": "4.0.3", 53 | "webpack": "4.43.0", 54 | "webpack-cli": "3.3.12", 55 | "webpack-dev-server": "3.11.3" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | const resolve = require('rollup-plugin-node-resolve'); 2 | const commonjs = require('rollup-plugin-commonjs'); 3 | const sourceMaps = require('rollup-plugin-sourcemaps'); 4 | const typescript = require('rollup-plugin-typescript2'); 5 | const { terser } = require('rollup-plugin-terser'); 6 | const pkg = require('./package.json'); 7 | 8 | export default { 9 | input: 'src/index.ts', 10 | output: [ 11 | { file: `dist/${pkg.name}.esm.js`, format: 'es', sourcemap: true }, 12 | { file: `dist/${pkg.name}.cjs.js`, format: 'cjs', sourcemap: true } 13 | ], 14 | external: Object.keys(pkg.peerDependencies), 15 | watch: { 16 | include: 'src/**' 17 | }, 18 | plugins: [ 19 | typescript({ useTsconfigDeclarationDir: true }), 20 | commonjs(), 21 | resolve(), 22 | sourceMaps(), 23 | terser() 24 | ] 25 | }; 26 | -------------------------------------------------------------------------------- /src/google-recaptcha-provider.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | createContext, 3 | ReactNode, 4 | useCallback, 5 | useEffect, 6 | useMemo, 7 | useRef, 8 | useState 9 | } from 'react'; 10 | import { 11 | cleanGoogleRecaptcha, 12 | injectGoogleReCaptchaScript, 13 | logWarningMessage 14 | } from './utils'; 15 | 16 | enum GoogleRecaptchaError { 17 | SCRIPT_NOT_AVAILABLE = 'Recaptcha script is not available' 18 | } 19 | 20 | interface IGoogleReCaptchaProviderProps { 21 | reCaptchaKey: string; 22 | language?: string; 23 | useRecaptchaNet?: boolean; 24 | useEnterprise?: boolean; 25 | scriptProps?: { 26 | nonce?: string; 27 | defer?: boolean; 28 | async?: boolean; 29 | appendTo?: 'head' | 'body'; 30 | id?: string; 31 | onLoadCallbackName?: string; 32 | }; 33 | container?: { 34 | element?: string | HTMLElement; 35 | parameters: { 36 | badge?: 'inline' | 'bottomleft' | 'bottomright'; 37 | theme?: 'dark' | 'light'; 38 | tabindex?: number; 39 | callback?: () => void; 40 | expiredCallback?: () => void; 41 | errorCallback?: () => void; 42 | } 43 | }; 44 | children: ReactNode; 45 | } 46 | 47 | export interface IGoogleReCaptchaConsumerProps { 48 | executeRecaptcha?: (action?: string) => Promise; 49 | container?: string | HTMLElement; 50 | } 51 | 52 | const GoogleReCaptchaContext = createContext({ 53 | executeRecaptcha: () => { 54 | // This default context function is not supposed to be called 55 | throw Error( 56 | 'GoogleReCaptcha Context has not yet been implemented, if you are using useGoogleReCaptcha hook, make sure the hook is called inside component wrapped by GoogleRecaptchaProvider' 57 | ); 58 | } 59 | }); 60 | 61 | const { Consumer: GoogleReCaptchaConsumer } = GoogleReCaptchaContext; 62 | 63 | export function GoogleReCaptchaProvider({ 64 | reCaptchaKey, 65 | useEnterprise = false, 66 | useRecaptchaNet = false, 67 | scriptProps, 68 | language, 69 | container, 70 | children 71 | }: IGoogleReCaptchaProviderProps) { 72 | const [greCaptchaInstance, setGreCaptchaInstance] = useState(null); 75 | const clientId = useRef(reCaptchaKey); 76 | 77 | const scriptPropsJson = JSON.stringify(scriptProps); 78 | const parametersJson = JSON.stringify(container?.parameters); 79 | 80 | useEffect(() => { 81 | if (!reCaptchaKey) { 82 | logWarningMessage( 83 | ' recaptcha key not provided' 84 | ); 85 | 86 | return; 87 | } 88 | 89 | const scriptId = scriptProps?.id || 'google-recaptcha-v3'; 90 | const onLoadCallbackName = scriptProps?.onLoadCallbackName || 'onRecaptchaLoadCallback'; 91 | 92 | ((window as unknown) as {[key: string]: () => void})[onLoadCallbackName] = () => { 93 | /* eslint-disable @typescript-eslint/no-explicit-any */ 94 | const grecaptcha = useEnterprise 95 | ? (window as any).grecaptcha.enterprise 96 | : (window as any).grecaptcha; 97 | 98 | const params = { 99 | badge: 'inline', 100 | size: 'invisible', 101 | sitekey: reCaptchaKey, 102 | ...(container?.parameters || {}) 103 | }; 104 | clientId.current = grecaptcha.render(container?.element, params); 105 | }; 106 | 107 | const onLoad = () => { 108 | if (!window || !(window as any).grecaptcha) { 109 | logWarningMessage( 110 | ` ${GoogleRecaptchaError.SCRIPT_NOT_AVAILABLE}` 111 | ); 112 | 113 | return; 114 | } 115 | 116 | const grecaptcha = useEnterprise 117 | ? (window as any).grecaptcha.enterprise 118 | : (window as any).grecaptcha; 119 | 120 | grecaptcha.ready(() => { 121 | setGreCaptchaInstance(grecaptcha); 122 | }); 123 | }; 124 | 125 | const onError = () => { 126 | logWarningMessage('Error loading google recaptcha script'); 127 | }; 128 | 129 | injectGoogleReCaptchaScript({ 130 | render: container?.element ? 'explicit' : reCaptchaKey, 131 | onLoadCallbackName, 132 | useEnterprise, 133 | useRecaptchaNet, 134 | scriptProps, 135 | language, 136 | onLoad, 137 | onError 138 | }); 139 | 140 | return () => { 141 | cleanGoogleRecaptcha(scriptId, container?.element); 142 | }; 143 | }, [ 144 | useEnterprise, 145 | useRecaptchaNet, 146 | scriptPropsJson, 147 | parametersJson, 148 | language, 149 | reCaptchaKey, 150 | container?.element, 151 | ]); 152 | 153 | const executeRecaptcha = useCallback( 154 | (action?: string) => { 155 | if (!greCaptchaInstance || !greCaptchaInstance.execute) { 156 | throw new Error( 157 | ' Google Recaptcha has not been loaded' 158 | ); 159 | } 160 | 161 | return greCaptchaInstance.execute(clientId.current, { action }); 162 | }, 163 | [greCaptchaInstance, clientId] 164 | ); 165 | 166 | const googleReCaptchaContextValue = useMemo( 167 | () => ({ 168 | executeRecaptcha: greCaptchaInstance ? executeRecaptcha : undefined, 169 | container: container?.element, 170 | }), 171 | [executeRecaptcha, greCaptchaInstance, container?.element] 172 | ); 173 | 174 | return ( 175 | 176 | {children} 177 | 178 | ); 179 | } 180 | 181 | export { GoogleReCaptchaConsumer, GoogleReCaptchaContext }; 182 | -------------------------------------------------------------------------------- /src/google-recaptcha.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useGoogleReCaptcha } from './use-google-recaptcha'; 3 | import { logWarningMessage } from './utils'; 4 | 5 | export interface IGoogleRecaptchaProps { 6 | onVerify: (token: string) => void | Promise; 7 | action?: string; 8 | refreshReCaptcha?: boolean | string | number | null; 9 | } 10 | 11 | export function GoogleReCaptcha({ 12 | action, 13 | onVerify, 14 | refreshReCaptcha, 15 | }: IGoogleRecaptchaProps) { 16 | const googleRecaptchaContextValue = useGoogleReCaptcha(); 17 | 18 | useEffect(() => { 19 | const { executeRecaptcha } = googleRecaptchaContextValue; 20 | 21 | if (!executeRecaptcha) { 22 | return; 23 | } 24 | 25 | const handleExecuteRecaptcha = async () => { 26 | const token = await executeRecaptcha(action); 27 | 28 | if (!onVerify) { 29 | logWarningMessage('Please define an onVerify function'); 30 | 31 | return; 32 | } 33 | 34 | onVerify(token); 35 | }; 36 | 37 | handleExecuteRecaptcha(); 38 | }, [action, onVerify, refreshReCaptcha, googleRecaptchaContextValue]); 39 | 40 | const { container } = googleRecaptchaContextValue; 41 | 42 | if (typeof container === 'string') { 43 | return
; 44 | } 45 | 46 | return null; 47 | } 48 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './google-recaptcha-provider'; 2 | export * from './google-recaptcha'; 3 | export * from './with-google-recaptcha'; 4 | export * from './use-google-recaptcha'; 5 | -------------------------------------------------------------------------------- /src/use-google-recaptcha.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { GoogleReCaptchaContext } from './google-recaptcha-provider'; 3 | 4 | export const useGoogleReCaptcha = () => useContext(GoogleReCaptchaContext); 5 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | interface IInjectGoogleReCaptchaScriptParams { 2 | render: string; 3 | onLoadCallbackName: string; 4 | useRecaptchaNet: boolean; 5 | useEnterprise: boolean; 6 | onLoad: () => void; 7 | onError: () => void; 8 | language?: string; 9 | scriptProps?: { 10 | nonce?: string; 11 | defer?: boolean; 12 | async?: boolean; 13 | appendTo?: 'head' | 'body'; 14 | id?: string; 15 | }; 16 | } 17 | 18 | /** 19 | * Function to generate the src for the script tag 20 | * 21 | * @param param0 22 | * @returns 23 | */ 24 | const generateGoogleRecaptchaSrc = ({ 25 | useRecaptchaNet, 26 | useEnterprise 27 | }: { 28 | useRecaptchaNet: boolean; 29 | useEnterprise: boolean; 30 | }) => { 31 | const hostName = useRecaptchaNet ? 'recaptcha.net' : 'google.com'; 32 | const script = useEnterprise ? 'enterprise.js' : 'api.js'; 33 | 34 | return `https://www.${hostName}/recaptcha/${script}`; 35 | }; 36 | 37 | /** 38 | * Function to clean the recaptcha_[language] script injected by the recaptcha.js 39 | */ 40 | const cleanGstaticRecaptchaScript = () => { 41 | const script = document.querySelector( 42 | 'script[src^="https://www.gstatic.com/recaptcha/releases"]' 43 | ); 44 | 45 | if (script) { 46 | script.remove(); 47 | } 48 | }; 49 | 50 | /** 51 | * Function to check if script has already been injected 52 | * 53 | * @param scriptId 54 | * @returns 55 | */ 56 | export const isScriptInjected = (scriptId: string) => 57 | !!document.querySelector(`#${scriptId}`); 58 | 59 | /** 60 | * Function to remove default badge 61 | * 62 | * @returns 63 | */ 64 | const removeDefaultBadge = () => { 65 | const nodeBadge = document.querySelector('.grecaptcha-badge'); 66 | if (nodeBadge && nodeBadge.parentNode) { 67 | document.body.removeChild(nodeBadge.parentNode); 68 | } 69 | }; 70 | 71 | /** 72 | * Function to clear custom badge 73 | * 74 | * @returns 75 | */ 76 | const cleanCustomBadge = (customBadge: HTMLElement | null) => { 77 | if (!customBadge) { 78 | return; 79 | } 80 | 81 | while (customBadge.lastChild) { 82 | customBadge.lastChild.remove(); 83 | } 84 | }; 85 | 86 | /** 87 | * Function to clean node of badge element 88 | * 89 | * @param container 90 | * @returns 91 | */ 92 | export const cleanBadge = (container?: HTMLElement | string) => { 93 | if (!container) { 94 | removeDefaultBadge(); 95 | 96 | return; 97 | } 98 | 99 | const customBadge = typeof container === 'string' ? document.getElementById(container) : container; 100 | 101 | cleanCustomBadge(customBadge); 102 | }; 103 | 104 | /** 105 | * Function to clean google recaptcha script 106 | * 107 | * @param scriptId 108 | * @param container 109 | */ 110 | export const cleanGoogleRecaptcha = (scriptId: string, container?: HTMLElement | string) => { 111 | // remove badge 112 | cleanBadge(container); 113 | 114 | // remove old config from window 115 | /* eslint-disable @typescript-eslint/no-explicit-any */ 116 | (window as any).___grecaptcha_cfg = undefined; 117 | 118 | // remove script 119 | const script = document.querySelector(`#${scriptId}`); 120 | if (script) { 121 | script.remove(); 122 | } 123 | 124 | cleanGstaticRecaptchaScript(); 125 | }; 126 | 127 | /** 128 | * Function to inject the google recaptcha script 129 | * 130 | * @param param0 131 | * @returns 132 | */ 133 | export const injectGoogleReCaptchaScript = ({ 134 | render, 135 | onLoadCallbackName, 136 | language, 137 | onLoad, 138 | useRecaptchaNet, 139 | useEnterprise, 140 | scriptProps: { 141 | nonce = '', 142 | defer = false, 143 | async = false, 144 | id = '', 145 | appendTo 146 | } = {} 147 | }: IInjectGoogleReCaptchaScriptParams) => { 148 | const scriptId = id || 'google-recaptcha-v3'; 149 | 150 | // Script has already been injected, just call onLoad and does othing else 151 | if (isScriptInjected(scriptId)) { 152 | onLoad(); 153 | 154 | return; 155 | } 156 | 157 | /** 158 | * Generate the js script 159 | */ 160 | const googleRecaptchaSrc = generateGoogleRecaptchaSrc({ 161 | useEnterprise, 162 | useRecaptchaNet 163 | }); 164 | const js = document.createElement('script'); 165 | js.id = scriptId; 166 | js.src = `${googleRecaptchaSrc}?render=${render}${ 167 | render === 'explicit' ? `&onload=${onLoadCallbackName}` : '' 168 | }${ 169 | language ? `&hl=${language}` : '' 170 | }`; 171 | 172 | if (!!nonce) { 173 | js.nonce = nonce; 174 | } 175 | 176 | js.defer = !!defer; 177 | js.async = !!async; 178 | js.onload = onLoad; 179 | 180 | /** 181 | * Append it to the body // head 182 | */ 183 | const elementToInjectScript = 184 | appendTo === 'body' 185 | ? document.body 186 | : document.getElementsByTagName('head')[0]; 187 | 188 | elementToInjectScript.appendChild(js); 189 | }; 190 | 191 | /** 192 | * Function to log warning message if it's not in production mode 193 | * 194 | * @param message String 195 | * @returns 196 | */ 197 | export const logWarningMessage = (message: string) => { 198 | const isDevelopmentMode = 199 | typeof process !== 'undefined' && !!process.env && process.env.NODE_ENV !== 'production'; 200 | 201 | if (isDevelopmentMode) { 202 | return; 203 | } 204 | 205 | console.warn(message); 206 | }; 207 | -------------------------------------------------------------------------------- /src/with-google-recaptcha.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import hoistNonReactStatics from 'hoist-non-react-statics'; 3 | import { ComponentType } from 'react'; 4 | import { 5 | GoogleReCaptchaConsumer, 6 | IGoogleReCaptchaConsumerProps 7 | } from './google-recaptcha-provider'; 8 | 9 | export interface IWithGoogleReCaptchaProps { 10 | googleReCaptchaProps: IGoogleReCaptchaConsumerProps; 11 | } 12 | 13 | // tslint:disable-next-line:only-arrow-functions 14 | export const withGoogleReCaptcha = function ( 15 | Component: ComponentType> 16 | ): ComponentType> { 17 | const WithGoogleReCaptchaComponent = ( 18 | props: OwnProps & Partial 19 | ) => ( 20 | 21 | {googleReCaptchaValues => ( 22 | 23 | )} 24 | 25 | ); 26 | 27 | WithGoogleReCaptchaComponent.displayName = `withGoogleReCaptcha(${ 28 | Component.displayName || Component.name || 'Component' 29 | })`; 30 | 31 | hoistNonReactStatics(WithGoogleReCaptchaComponent, Component); 32 | 33 | return WithGoogleReCaptchaComponent; 34 | }; 35 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "ES2015", 5 | "lib": ["es2015", "es7", "dom"], 6 | "jsx": "react", 7 | "declaration": true, 8 | "declarationMap": true, 9 | "declarationDir": "./dist/types", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "strict": true, 13 | "noUnusedLocals": true, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "moduleResolution": "node", 17 | "baseUrl": "./", 18 | "experimentalDecorators": true, 19 | "emitDecoratorMetadata": true, 20 | "allowSyntheticDefaultImports": true 21 | }, 22 | "include": ["src"], 23 | "compileOnSave": true 24 | } 25 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended", "tslint-react"], 4 | "rules": { 5 | "quotemark": [ 6 | true, 7 | "single", 8 | "jsx-double", 9 | "avoid-escape", 10 | "avoid-template" 11 | ], 12 | "member-access": false, 13 | "trailing-comma": false, 14 | "no-console": false, 15 | "arrow-parens": [true, "ban-single-arg-parens"], 16 | "interface-over-type-literal": false, 17 | "jsx-no-multiline-js": false, 18 | "jsx-boolean-value": false, 19 | "object-literal-sort-keys": false 20 | }, 21 | "rulesDirectory": [] 22 | } 23 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const Dotenv = require('dotenv-webpack'); 3 | 4 | module.exports = { 5 | mode: 'development', 6 | entry: './example/index.tsx', 7 | output: { 8 | filename: 'bundle.js', 9 | path: __dirname + '/build' 10 | }, 11 | devtool: 'source-map', 12 | resolve: { 13 | extensions: ['.ts', '.tsx', '.js', '.json'], 14 | alias: { react: path.resolve(__dirname, 'node_modules/react') } 15 | }, 16 | module: { 17 | rules: [ 18 | { test: /\.tsx?$/, loader: 'awesome-typescript-loader' }, 19 | { enforce: 'pre', test: /\.js$/, loader: 'source-map-loader' } 20 | ] 21 | }, 22 | plugins: [new Dotenv()] 23 | }; 24 | --------------------------------------------------------------------------------