Support for Google reCaptcha v2 in Button Widget (#5638)

* Handle google recaptcha v2 in button component

* Use same code for recaptcha v2 and v3

* Updated error handling comments

* Added toggle to use google recaptcha v2 with button

* Create separate components for Google recaptcha v2 and v3

* Extract click function from google recaptch v3 component

* Hide recaptcha error badge and show invalid site key error on button key

* Fix isInvalidKey name
This commit is contained in:
Vicky Bansal 2021-07-08 17:32:08 +05:30 committed by GitHub
parent 4ffeca4a56
commit 718c257286
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 12903 additions and 12273 deletions

View File

@ -109,6 +109,7 @@
"react-dnd-touch-backend": "^9.4.0",
"react-dom": "^16.7.0",
"react-google-maps": "^9.4.5",
"react-google-recaptcha": "^2.1.0",
"react-helmet": "^5.2.1",
"react-infinite-scroller": "^1.2.4",
"react-instantsearch-dom": "^6.4.0",
@ -208,6 +209,7 @@
"@types/marked": "^1.2.2",
"@types/node-forge": "^0.10.0",
"@types/react-beautiful-dnd": "^11.0.4",
"@types/react-google-recaptcha": "^2.1.1",
"@types/react-select": "^3.0.5",
"@types/react-tabs": "^2.3.1",
"@types/react-test-renderer": "^17.0.1",

View File

@ -1,4 +1,4 @@
import React from "react";
import React, { useRef, useState } from "react";
import {
IButtonProps,
MaybeElement,
@ -18,6 +18,7 @@ import {
} from "constants/messages";
import { Variant } from "components/ads/common";
import { Toaster } from "components/ads/Toast";
import ReCAPTCHA from "react-google-recaptcha";
const getButtonColorStyles = (props: { theme: Theme } & ButtonStyleProps) => {
if (props.filled) return props.theme.colors.textOnDarkBG;
@ -35,6 +36,14 @@ const ButtonColorStyles = css<ButtonStyleProps>`
fill: ${getButtonColorStyles};
}
`;
const RecaptchaWrapper = styled.div`
position: relative;
.grecaptcha-badge {
visibility: hidden;
}
`;
const AccentColorMap: Record<ButtonStyleName, string> = {
primary: "primaryOld",
secondary: "secondaryOld",
@ -144,6 +153,7 @@ export enum ButtonType {
interface RecaptchaProps {
googleRecaptchaKey?: string;
clickWithRecaptcha: (token: string) => void;
recaptchaV2?: boolean;
}
interface ButtonContainerProps extends ComponentProps {
@ -170,20 +180,56 @@ const mapButtonStyleToStyleName = (buttonStyle?: ButtonStyle) => {
}
};
function RecaptchaComponent(
function RecaptchaV2Component(
props: {
children: any;
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
recaptchaV2?: boolean;
handleError: (event: React.MouseEvent<HTMLElement>, error: string) => void;
} & RecaptchaProps,
) {
function handleError(event: React.MouseEvent<HTMLElement>, error: string) {
Toaster.show({
text: error,
variant: Variant.danger,
});
props.onClick && props.onClick(event);
}
const recaptchaRef = useRef<ReCAPTCHA>(null);
const [isInvalidKey, setInvalidKey] = useState(false);
const handleBtnClick = async (event: React.MouseEvent<HTMLElement>) => {
if (isInvalidKey) {
// Handle incorrent google recaptcha site key
props.handleError(event, createMessage(GOOGLE_RECAPTCHA_KEY_ERROR));
} else {
try {
const token = await recaptchaRef?.current?.executeAsync();
if (token) {
props.clickWithRecaptcha(token);
} else {
// Handle incorrent google recaptcha site key
props.handleError(event, createMessage(GOOGLE_RECAPTCHA_KEY_ERROR));
}
} catch (err) {
// Handle error due to google recaptcha key of different domain
props.handleError(event, createMessage(GOOGLE_RECAPTCHA_DOMAIN_ERROR));
}
}
};
return (
<RecaptchaWrapper onClick={handleBtnClick}>
{props.children}
<ReCAPTCHA
onErrored={() => setInvalidKey(true)}
ref={recaptchaRef}
sitekey={props.googleRecaptchaKey || ""}
size="invisible"
/>
</RecaptchaWrapper>
);
}
function RecaptchaV3Component(
props: {
children: any;
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
recaptchaV2?: boolean;
handleError: (event: React.MouseEvent<HTMLElement>, error: string) => void;
} & RecaptchaProps,
) {
// Check if a string is a valid JSON string
const checkValidJson = (inputString: string): boolean => {
try {
@ -194,6 +240,35 @@ function RecaptchaComponent(
}
};
const handleBtnClick = (event: React.MouseEvent<HTMLElement>) => {
if (status === ScriptStatus.READY) {
(window as any).grecaptcha.ready(() => {
try {
(window as any).grecaptcha
.execute(props.googleRecaptchaKey, {
action: "submit",
})
.then((token: any) => {
props.clickWithRecaptcha(token);
})
.catch(() => {
// Handle incorrent google recaptcha site key
props.handleError(
event,
createMessage(GOOGLE_RECAPTCHA_KEY_ERROR),
);
});
} catch (err) {
// Handle error due to google recaptcha key of different domain
props.handleError(
event,
createMessage(GOOGLE_RECAPTCHA_DOMAIN_ERROR),
);
}
});
}
};
let validGoogleRecaptchaKey = props.googleRecaptchaKey;
if (validGoogleRecaptchaKey && checkValidJson(validGoogleRecaptchaKey)) {
@ -203,32 +278,7 @@ function RecaptchaComponent(
const status = useScript(
`https://www.google.com/recaptcha/api.js?render=${validGoogleRecaptchaKey}`,
);
return (
<div
onClick={(event: React.MouseEvent<HTMLElement>) => {
if (status === ScriptStatus.READY) {
(window as any).grecaptcha.ready(() => {
try {
(window as any).grecaptcha
.execute(props.googleRecaptchaKey, { action: "submit" })
.then((token: any) => {
props.clickWithRecaptcha(token);
})
.catch(() => {
// Handle corrent key with wrong
handleError(event, createMessage(GOOGLE_RECAPTCHA_KEY_ERROR));
});
} catch (ex) {
// Handle wrong key
handleError(event, createMessage(GOOGLE_RECAPTCHA_DOMAIN_ERROR));
}
});
}
}}
>
{props.children}
</div>
);
return <div onClick={handleBtnClick}>{props.children}</div>;
}
function BtnWrapper(
@ -239,7 +289,23 @@ function BtnWrapper(
) {
if (!props.googleRecaptchaKey)
return <div onClick={props.onClick}>{props.children}</div>;
return <RecaptchaComponent {...props} />;
else {
const handleError = (
event: React.MouseEvent<HTMLElement>,
error: string,
) => {
Toaster.show({
text: error,
variant: Variant.danger,
});
props.onClick && props.onClick(event);
};
if (props.recaptchaV2) {
return <RecaptchaV2Component {...props} handleError={handleError} />;
} else {
return <RecaptchaV3Component {...props} handleError={handleError} />;
}
}
}
// To be used with the canvas
@ -251,6 +317,7 @@ function ButtonContainer(
clickWithRecaptcha={props.clickWithRecaptcha}
googleRecaptchaKey={props.googleRecaptchaKey}
onClick={props.onClick}
recaptchaV2={props.recaptchaV2}
>
<BaseButton
accent={mapButtonStyleToStyleName(props.buttonStyle)}

View File

@ -28,6 +28,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
isDisabled: false,
isVisible: true,
isDefaultClickDisabled: true,
recaptchaV2: false,
version: 1,
},
TEXT_WIDGET: {
@ -637,6 +638,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
widgetName: "FormButton",
text: "Submit",
isDefaultClickDisabled: true,
recaptchaV2: false,
version: 1,
},
FORM_WIDGET: {
@ -686,6 +688,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
buttonStyle: "PRIMARY_BUTTON",
disabledWhenInvalid: true,
resetFormOnClick: true,
recaptchaV2: false,
version: 1,
},
},
@ -704,6 +707,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
buttonStyle: "SECONDARY_BUTTON",
disabledWhenInvalid: false,
resetFormOnClick: true,
recaptchaV2: false,
version: 1,
},
},

View File

@ -88,6 +88,16 @@ class ButtonWidget extends BaseWidget<ButtonWidgetProps, ButtonWidgetState> {
isTriggerProperty: false,
validation: VALIDATION_TYPES.TEXT,
},
{
propertyName: "recaptchaV2",
label: "Google reCAPTCHA v2",
controlType: "SWITCH",
helpText: "Use reCAPTCHA v2",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: VALIDATION_TYPES.BOOLEAN,
},
],
},
{
@ -163,6 +173,7 @@ class ButtonWidget extends BaseWidget<ButtonWidgetProps, ButtonWidgetState> {
isLoading={this.props.isLoading || this.state.isLoading}
key={this.props.widgetId}
onClick={!this.props.isDisabled ? this.onButtonClickBound : undefined}
recaptchaV2={this.props.recaptchaV2}
text={this.props.text}
type={this.props.buttonType || ButtonType.BUTTON}
widgetId={this.props.widgetId}
@ -188,6 +199,7 @@ export interface ButtonWidgetProps extends WidgetProps, WithMeta {
onClick?: string;
isDisabled?: boolean;
isVisible?: boolean;
recaptchaV2?: boolean;
buttonType?: ButtonType;
googleRecaptchaKey?: string;
}

View File

@ -105,6 +105,16 @@ class FormButtonWidget extends BaseWidget<
isTriggerProperty: false,
validation: VALIDATION_TYPES.TEXT,
},
{
propertyName: "recaptchaV2",
label: "Google reCAPTCHA v2",
controlType: "SWITCH",
helpText: "Use reCAPTCHA v2",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: VALIDATION_TYPES.BOOLEAN,
},
],
},
{
@ -189,6 +199,7 @@ class FormButtonWidget extends BaseWidget<
isLoading={this.props.isLoading || this.state.isLoading}
key={this.props.widgetId}
onClick={!disabled ? this.onButtonClickBound : undefined}
recaptchaV2={this.props.recaptchaV2}
text={this.props.text}
type={this.props.buttonType || ButtonType.BUTTON}
widgetId={this.props.widgetId}
@ -219,6 +230,7 @@ export interface FormButtonWidgetProps extends WidgetProps, WithMeta {
onReset?: () => void;
disabledWhenInvalid?: boolean;
googleRecaptchaKey?: string;
recaptchaV2?: boolean;
}
export interface FormButtonWidgetState extends WidgetState {

File diff suppressed because it is too large Load Diff