Skip to content

Commit 321d392

Browse files
authored
Merge pull request #118 from ramirezcgn/inline-badge
Add custom inline-badge
2 parents 5877d8e + bde21c1 commit 321d392

File tree

5 files changed

+182
-41
lines changed

5 files changed

+182
-41
lines changed

README.md

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,15 @@ Usually, your application only needs one provider. You should place it as high a
4141

4242
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.
4343

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) |
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) |
5153

5254
```javascript
5355
import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3';
@@ -64,6 +66,13 @@ ReactDom.render(
6466
appendTo: 'head', // optional, default to "head", can be "head" or "body",
6567
nonce: undefined // optional, default undefined
6668
}}
69+
container={{ // optional to render inside custom element
70+
element: "[required_id_or_htmlelement]",
71+
parameters: {
72+
badge: '[inline|bottomright|bottomleft]', // optional, default undefined
73+
theme: 'dark', // optional, default undefined
74+
}
75+
}}
6776
>
6877
<YourApp />
6978
</GoogleReCaptchaProvider>,
@@ -75,7 +84,7 @@ There are three ways to trigger the recaptcha validation: using the `GoogleReCap
7584

7685
#### GoogleReCaptcha
7786

78-
`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.
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.
7988

8089
```javascript
8190
import {
@@ -113,6 +122,36 @@ const MyComponent: FC = () => {
113122
};
114123
```
115124

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+
<div>
143+
<GoogleReCaptcha
144+
onVerify={onVerify}
145+
refreshReCaptcha={refreshReCaptcha}
146+
/>
147+
<button onClick={doSomething}>
148+
Do Something
149+
</button>
150+
</div>
151+
);
152+
};
153+
```
154+
116155
#### React Hook: useGoogleReCaptcha (recommended approach)
117156

118157
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`.

example/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { WithGoogleRecaptchaExample } from './with-google-recaptcha-example';
77
ReactDom.render(
88
<GoogleReCaptchaProvider
99
useRecaptchaNet
10-
reCaptchaKey={process.env.RECAPTCHA_KEY}
10+
reCaptchaKey={process.env.RECAPTCHA_KEY as string}
1111
scriptProps={{ async: true, defer: true, appendTo: 'body' }}
1212
>
1313
<h2>Google Recaptcha Example</h2>

src/google-recaptcha-provider.tsx

Lines changed: 58 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import React from 'react';
2-
import {
3-
useMemo,
4-
useState,
5-
useEffect,
6-
useCallback,
1+
import React, {
72
createContext,
8-
ReactNode
3+
ReactNode,
4+
useCallback,
5+
useEffect,
6+
useMemo,
7+
useRef,
8+
useState
99
} from 'react';
1010
import {
1111
cleanGoogleRecaptcha,
@@ -18,7 +18,7 @@ enum GoogleRecaptchaError {
1818
}
1919

2020
interface IGoogleReCaptchaProviderProps {
21-
reCaptchaKey?: string;
21+
reCaptchaKey: string;
2222
language?: string;
2323
useRecaptchaNet?: boolean;
2424
useEnterprise?: boolean;
@@ -28,12 +28,25 @@ interface IGoogleReCaptchaProviderProps {
2828
async?: boolean;
2929
appendTo?: 'head' | 'body';
3030
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+
}
3143
};
3244
children: ReactNode;
3345
}
3446

3547
export interface IGoogleReCaptchaConsumerProps {
3648
executeRecaptcha?: (action?: string) => Promise<string>;
49+
container?: string | HTMLElement;
3750
}
3851

3952
const GoogleReCaptchaContext = createContext<IGoogleReCaptchaConsumerProps>({
@@ -53,13 +66,16 @@ export function GoogleReCaptchaProvider({
5366
useRecaptchaNet = false,
5467
scriptProps,
5568
language,
69+
container,
5670
children
5771
}: IGoogleReCaptchaProviderProps) {
5872
const [greCaptchaInstance, setGreCaptchaInstance] = useState<null | {
5973
execute: Function;
6074
}>(null);
75+
const clientId = useRef<number | string>(reCaptchaKey);
6176

6277
const scriptPropsJson = JSON.stringify(scriptProps);
78+
const parametersJson = JSON.stringify(container?.parameters);
6379

6480
useEffect(() => {
6581
if (!reCaptchaKey) {
@@ -71,6 +87,22 @@ export function GoogleReCaptchaProvider({
7187
}
7288

7389
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+
};
74106

75107
const onLoad = () => {
76108
if (!window || !(window as any).grecaptcha) {
@@ -95,7 +127,8 @@ export function GoogleReCaptchaProvider({
95127
};
96128

97129
injectGoogleReCaptchaScript({
98-
reCaptchaKey,
130+
render: container?.element ? 'explicit' : reCaptchaKey,
131+
onLoadCallbackName,
99132
useEnterprise,
100133
useRecaptchaNet,
101134
scriptProps,
@@ -105,30 +138,37 @@ export function GoogleReCaptchaProvider({
105138
});
106139

107140
return () => {
108-
cleanGoogleRecaptcha(scriptId);
141+
cleanGoogleRecaptcha(scriptId, container?.element);
109142
};
110-
}, [useEnterprise, useRecaptchaNet, scriptPropsJson, language, reCaptchaKey]);
143+
}, [
144+
useEnterprise,
145+
useRecaptchaNet,
146+
scriptPropsJson,
147+
parametersJson,
148+
language,
149+
reCaptchaKey,
150+
container?.element,
151+
]);
111152

112153
const executeRecaptcha = useCallback(
113-
async (action?: string) => {
154+
(action?: string) => {
114155
if (!greCaptchaInstance || !greCaptchaInstance.execute) {
115156
throw new Error(
116157
'<GoogleReCaptchaProvider /> Google Recaptcha has not been loaded'
117158
);
118159
}
119160

120-
const result = await greCaptchaInstance.execute(reCaptchaKey, { action });
121-
122-
return result;
161+
return greCaptchaInstance.execute(clientId.current, { action });
123162
},
124-
[greCaptchaInstance]
163+
[greCaptchaInstance, clientId]
125164
);
126165

127166
const googleReCaptchaContextValue = useMemo(
128167
() => ({
129-
executeRecaptcha: greCaptchaInstance ? executeRecaptcha : undefined
168+
executeRecaptcha: greCaptchaInstance ? executeRecaptcha : undefined,
169+
container: container?.element,
130170
}),
131-
[executeRecaptcha, greCaptchaInstance]
171+
[executeRecaptcha, greCaptchaInstance, container?.element]
132172
);
133173

134174
return (

src/google-recaptcha.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
1-
import { useEffect } from 'react';
1+
import React, { useEffect } from 'react';
22
import { useGoogleReCaptcha } from './use-google-recaptcha';
33
import { logWarningMessage } from './utils';
44

55
export interface IGoogleRecaptchaProps {
66
onVerify: (token: string) => void | Promise<void>;
77
action?: string;
8+
refreshReCaptcha?: boolean | string | number | null;
89
}
910

10-
export function GoogleReCaptcha({ action, onVerify }: IGoogleRecaptchaProps) {
11+
export function GoogleReCaptcha({
12+
action,
13+
onVerify,
14+
refreshReCaptcha,
15+
}: IGoogleRecaptchaProps) {
1116
const googleRecaptchaContextValue = useGoogleReCaptcha();
1217

1318
useEffect(() => {
@@ -30,7 +35,13 @@ export function GoogleReCaptcha({ action, onVerify }: IGoogleRecaptchaProps) {
3035
};
3136

3237
handleExecuteRecaptcha();
33-
}, [action, onVerify, googleRecaptchaContextValue]);
38+
}, [action, onVerify, refreshReCaptcha, googleRecaptchaContextValue]);
39+
40+
const { container } = googleRecaptchaContextValue;
41+
42+
if (typeof container === 'string') {
43+
return <div id={container} />;
44+
}
3445

3546
return null;
3647
}

src/utils.ts

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
interface InjectGoogleReCaptchaScriptParams {
2-
reCaptchaKey: string;
1+
interface IInjectGoogleReCaptchaScriptParams {
2+
render: string;
3+
onLoadCallbackName: string;
34
useRecaptchaNet: boolean;
45
useEnterprise: boolean;
56
onLoad: () => void;
@@ -38,7 +39,7 @@ const generateGoogleRecaptchaSrc = ({
3839
*/
3940
const cleanGstaticRecaptchaScript = () => {
4041
const script = document.querySelector(
41-
`script[src^='https://www.gstatic.com/recaptcha/releases']`
42+
'script[src^="https://www.gstatic.com/recaptcha/releases"]'
4243
);
4344

4445
if (script) {
@@ -56,16 +57,63 @@ export const isScriptInjected = (scriptId: string) =>
5657
!!document.querySelector(`#${scriptId}`);
5758

5859
/**
59-
* Function to clean google recaptcha script
60+
* Function to remove default badge
6061
*
61-
* @param scriptId
62+
* @returns
6263
*/
63-
export const cleanGoogleRecaptcha = (scriptId: string) => {
64-
// remove badge
64+
const removeDefaultBadge = () => {
6565
const nodeBadge = document.querySelector('.grecaptcha-badge');
6666
if (nodeBadge && nodeBadge.parentNode) {
6767
document.body.removeChild(nodeBadge.parentNode);
6868
}
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;
69117

70118
// remove script
71119
const script = document.querySelector(`#${scriptId}`);
@@ -83,7 +131,8 @@ export const cleanGoogleRecaptcha = (scriptId: string) => {
83131
* @returns
84132
*/
85133
export const injectGoogleReCaptchaScript = ({
86-
reCaptchaKey,
134+
render,
135+
onLoadCallbackName,
87136
language,
88137
onLoad,
89138
useRecaptchaNet,
@@ -93,9 +142,9 @@ export const injectGoogleReCaptchaScript = ({
93142
defer = false,
94143
async = false,
95144
id = '',
96-
appendTo = undefined
145+
appendTo
97146
} = {}
98-
}: InjectGoogleReCaptchaScriptParams) => {
147+
}: IInjectGoogleReCaptchaScriptParams) => {
99148
const scriptId = id || 'google-recaptcha-v3';
100149

101150
// Script has already been injected, just call onLoad and does othing else
@@ -114,7 +163,9 @@ export const injectGoogleReCaptchaScript = ({
114163
});
115164
const js = document.createElement('script');
116165
js.id = scriptId;
117-
js.src = `${googleRecaptchaSrc}?render=${reCaptchaKey}${
166+
js.src = `${googleRecaptchaSrc}?render=${render}${
167+
render === 'explicit' ? `&onload=${onLoadCallbackName}` : ''
168+
}${
118169
language ? `&hl=${language}` : ''
119170
}`;
120171

0 commit comments

Comments
 (0)