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:
parent
4ffeca4a56
commit
718c257286
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
25007
app/client/yarn.lock
25007
app/client/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user