Google Recaptcha integration for button and form button widgets. (#1118)
* Adding base code for google re-captcha. * Removing recaptcha as well * Adding recaptchaToken on Button. * Fixing updateMetaProperty errors. * Handling recaptcha generation failed case. * Adding a message for recaptcha token gen fail * Rename setRecaptchaToken * Adding loading state for recaptcha button * Adding googleRecaptchaKey as a Btn prop * Adding the bound functions in widgets * Removing unused vars. * Handling google recaptcha key error. * Adding proper messages for
This commit is contained in:
parent
f9f85a8897
commit
8bf80fe507
|
|
@ -207,4 +207,4 @@
|
|||
"pre-commit": "lint-staged"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,12 @@ import { ButtonStyle } from "widgets/ButtonWidget";
|
|||
import { Theme, darkenHover, darkenActive } from "constants/DefaultTheme";
|
||||
import _ from "lodash";
|
||||
import { ComponentProps } from "components/designSystems/appsmith/BaseComponent";
|
||||
import useScript from "utils/hooks/useScript";
|
||||
import { AppToaster } from "components/editorComponents/ToastComponent";
|
||||
import {
|
||||
GOOGLE_RECAPTCHA_KEY_ERROR,
|
||||
GOOGLE_RECAPTCHA_DOMAIN_ERROR,
|
||||
} from "constants/messages";
|
||||
|
||||
const getButtonColorStyles = (props: { theme: Theme } & ButtonStyleProps) => {
|
||||
if (props.filled) return props.theme.colors.textOnDarkBG;
|
||||
|
|
@ -124,6 +130,11 @@ export enum ButtonType {
|
|||
BUTTON = "button",
|
||||
}
|
||||
|
||||
interface RecaptchaProps {
|
||||
googleRecaptchaKey?: string;
|
||||
clickWithRecaptcha: (token: string) => void;
|
||||
}
|
||||
|
||||
interface ButtonContainerProps extends ComponentProps {
|
||||
text?: string;
|
||||
icon?: MaybeElement;
|
||||
|
|
@ -148,20 +159,82 @@ const mapButtonStyleToStyleName = (buttonStyle?: ButtonStyle) => {
|
|||
}
|
||||
};
|
||||
|
||||
// To be used with the canvas
|
||||
const ButtonContainer = (props: ButtonContainerProps & ButtonStyleProps) => {
|
||||
const RecaptchaComponent = (
|
||||
props: {
|
||||
children: any;
|
||||
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
|
||||
} & RecaptchaProps,
|
||||
) => {
|
||||
function handleError(event: React.MouseEvent<HTMLElement>, error: string) {
|
||||
AppToaster.show({
|
||||
message: error,
|
||||
type: "error",
|
||||
});
|
||||
props.onClick && props.onClick(event);
|
||||
}
|
||||
const status = useScript(
|
||||
`https://www.google.com/recaptcha/api.js?render=${props.googleRecaptchaKey}`,
|
||||
);
|
||||
return (
|
||||
<BaseButton
|
||||
loading={props.isLoading}
|
||||
icon={props.icon}
|
||||
rightIcon={props.rightIcon}
|
||||
text={props.text}
|
||||
filled={props.buttonStyle !== "SECONDARY_BUTTON"}
|
||||
accent={mapButtonStyleToStyleName(props.buttonStyle)}
|
||||
<div
|
||||
onClick={(event: React.MouseEvent<HTMLElement>) => {
|
||||
if (status === "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, GOOGLE_RECAPTCHA_KEY_ERROR);
|
||||
});
|
||||
} catch (ex) {
|
||||
// Handle wrong key
|
||||
handleError(event, GOOGLE_RECAPTCHA_DOMAIN_ERROR);
|
||||
}
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const BtnWrapper = (
|
||||
props: {
|
||||
children: any;
|
||||
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
|
||||
} & RecaptchaProps,
|
||||
) => {
|
||||
if (!props.googleRecaptchaKey)
|
||||
return <div onClick={props.onClick}>{props.children}</div>;
|
||||
return <RecaptchaComponent {...props}></RecaptchaComponent>;
|
||||
};
|
||||
|
||||
// To be used with the canvas
|
||||
const ButtonContainer = (
|
||||
props: ButtonContainerProps & ButtonStyleProps & RecaptchaProps,
|
||||
) => {
|
||||
return (
|
||||
<BtnWrapper
|
||||
googleRecaptchaKey={props.googleRecaptchaKey}
|
||||
clickWithRecaptcha={props.clickWithRecaptcha}
|
||||
onClick={props.onClick}
|
||||
disabled={props.disabled}
|
||||
type={props.type}
|
||||
/>
|
||||
>
|
||||
<BaseButton
|
||||
loading={props.isLoading}
|
||||
icon={props.icon}
|
||||
rightIcon={props.rightIcon}
|
||||
text={props.text}
|
||||
filled={props.buttonStyle !== "SECONDARY_BUTTON"}
|
||||
accent={mapButtonStyleToStyleName(props.buttonStyle)}
|
||||
disabled={props.disabled}
|
||||
type={props.type}
|
||||
/>
|
||||
</BtnWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -163,3 +163,7 @@ export const TABLE_FILTER_COLUMN_TYPE_CALLOUT =
|
|||
export const WIDGET_SIDEBAR_TITLE = "Widgets";
|
||||
export const WIDGET_SIDEBAR_CAPTION =
|
||||
"To add a widget, please drag and drop a widget on the canvas to the right";
|
||||
export const GOOGLE_RECAPTCHA_KEY_ERROR =
|
||||
"Google Re-Captcha Token Generation failed! Please check the Re-captcha Site Key.";
|
||||
export const GOOGLE_RECAPTCHA_DOMAIN_ERROR =
|
||||
"Google Re-Captcha Token Generation failed! Please check the allowed domains.";
|
||||
|
|
|
|||
|
|
@ -806,6 +806,14 @@ const PropertyPaneConfigResponse: PropertyPaneConfigsResponse["data"] = {
|
|||
controlType: "SWITCH",
|
||||
isJSConvertible: true,
|
||||
},
|
||||
{
|
||||
id: "15.1.6",
|
||||
propertyName: "googleRecaptchaKey",
|
||||
label: "Google Recaptcha Key",
|
||||
helpText: "Sets Google Recaptcha v3 site key for button",
|
||||
controlType: "INPUT_TEXT",
|
||||
placeholderText: "Enter google recaptcha key",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -954,6 +962,14 @@ const PropertyPaneConfigResponse: PropertyPaneConfigsResponse["data"] = {
|
|||
helpText: "Disables clicks to this widget",
|
||||
isJSConvertible: true,
|
||||
},
|
||||
{
|
||||
id: "1.1.4",
|
||||
propertyName: "googleRecaptchaKey",
|
||||
label: "Google Recaptcha Key",
|
||||
helpText: "Sets Google Recaptcha v3 site key for button",
|
||||
controlType: "INPUT_TEXT",
|
||||
placeholderText: "Enter google recaptcha key",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -113,6 +113,8 @@ export const entityDefinitions = {
|
|||
isVisible: isVisible,
|
||||
text: "string",
|
||||
isDisabled: "bool",
|
||||
recaptchaToken: "string",
|
||||
googleRecaptchaKey: "string",
|
||||
},
|
||||
DATE_PICKER_WIDGET: {
|
||||
"!doc":
|
||||
|
|
@ -176,6 +178,8 @@ export const entityDefinitions = {
|
|||
isVisible: isVisible,
|
||||
text: "string",
|
||||
isDisabled: "bool",
|
||||
recaptchaToken: "string",
|
||||
googleRecaptchaKey: "string",
|
||||
},
|
||||
MAP_WIDGET: {
|
||||
isVisible: isVisible,
|
||||
|
|
|
|||
69
app/client/src/utils/hooks/useScript.tsx
Normal file
69
app/client/src/utils/hooks/useScript.tsx
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import { useState, useEffect } from "react";
|
||||
|
||||
// Hook
|
||||
export default function useScript(src: string) {
|
||||
// Keep track of script status ("idle", "loading", "ready", "error")
|
||||
const [status, setStatus] = useState(src ? "loading" : "idle");
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
// Allow falsy src value if waiting on other data needed for
|
||||
// constructing the script URL passed to this hook.
|
||||
if (!src) {
|
||||
setStatus("idle");
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch existing script element by src
|
||||
// It may have been added by another intance of this hook
|
||||
let script = document.querySelector(`script[src="${src}"]`) as any;
|
||||
|
||||
if (!script) {
|
||||
// Create script
|
||||
script = document.createElement("script");
|
||||
script.src = src;
|
||||
script.async = true;
|
||||
script.setAttribute("data-status", "loading");
|
||||
// Add script to document body
|
||||
document.body.appendChild(script);
|
||||
|
||||
// Store status in attribute on script
|
||||
// This can be read by other instances of this hook
|
||||
const setAttributeFromEvent = (event: any) => {
|
||||
script.setAttribute(
|
||||
"data-status",
|
||||
event.type === "load" ? "ready" : "error",
|
||||
);
|
||||
};
|
||||
|
||||
script.addEventListener("load", setAttributeFromEvent);
|
||||
script.addEventListener("error", setAttributeFromEvent);
|
||||
} else {
|
||||
// Grab existing script status from attribute and set to state.
|
||||
setStatus(script.getAttribute("data-status"));
|
||||
}
|
||||
|
||||
// Script event handler to update status in state
|
||||
// Note: Even if the script already exists we still need to add
|
||||
// event handlers to update the state for *this* hook instance.
|
||||
const setStateFromEvent = (event: any) => {
|
||||
setStatus(event.type === "load" ? "ready" : "error");
|
||||
};
|
||||
|
||||
// Add event listeners
|
||||
script.addEventListener("load", setStateFromEvent);
|
||||
script.addEventListener("error", setStateFromEvent);
|
||||
|
||||
// Remove event listeners on cleanup
|
||||
return () => {
|
||||
if (script) {
|
||||
script.removeEventListener("load", setStateFromEvent);
|
||||
script.removeEventListener("error", setStateFromEvent);
|
||||
}
|
||||
};
|
||||
},
|
||||
[src], // Only re-run effect if script src changes
|
||||
);
|
||||
|
||||
return status;
|
||||
}
|
||||
|
|
@ -12,13 +12,15 @@ import {
|
|||
import { VALIDATION_TYPES } from "constants/WidgetValidation";
|
||||
import { TriggerPropertiesMap } from "utils/WidgetFactory";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import withMeta, { WithMeta } from "./MetaHOC";
|
||||
|
||||
class ButtonWidget extends BaseWidget<ButtonWidgetProps, ButtonWidgetState> {
|
||||
onButtonClickBound: (event: React.MouseEvent<HTMLElement>) => void;
|
||||
|
||||
clickWithRecaptchaBound: (token: string) => void;
|
||||
constructor(props: ButtonWidgetProps) {
|
||||
super(props);
|
||||
this.onButtonClickBound = this.onButtonClick.bind(this);
|
||||
this.clickWithRecaptchaBound = this.clickWithRecaptcha.bind(this);
|
||||
this.state = {
|
||||
isLoading: false,
|
||||
};
|
||||
|
|
@ -38,6 +40,11 @@ class ButtonWidget extends BaseWidget<ButtonWidgetProps, ButtonWidgetState> {
|
|||
onClick: true,
|
||||
};
|
||||
}
|
||||
static getMetaPropertiesMap(): Record<string, any> {
|
||||
return {
|
||||
recaptchaToken: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
onButtonClick() {
|
||||
if (this.props.onClick) {
|
||||
|
|
@ -54,6 +61,21 @@ class ButtonWidget extends BaseWidget<ButtonWidgetProps, ButtonWidgetState> {
|
|||
}
|
||||
}
|
||||
|
||||
clickWithRecaptcha(token: string) {
|
||||
if (this.props.onClick) {
|
||||
this.setState({
|
||||
isLoading: true,
|
||||
});
|
||||
}
|
||||
this.props.updateWidgetMetaProperty("recaptchaToken", token, {
|
||||
dynamicString: this.props.onClick,
|
||||
event: {
|
||||
type: EventType.ON_CLICK,
|
||||
callback: this.handleActionComplete,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
handleActionComplete = () => {
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
|
|
@ -72,6 +94,8 @@ class ButtonWidget extends BaseWidget<ButtonWidgetProps, ButtonWidgetState> {
|
|||
onClick={this.onButtonClickBound}
|
||||
isLoading={this.props.isLoading || this.state.isLoading}
|
||||
type={this.props.buttonType || ButtonType.BUTTON}
|
||||
googleRecaptchaKey={this.props.googleRecaptchaKey}
|
||||
clickWithRecaptcha={this.clickWithRecaptchaBound}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -87,13 +111,14 @@ export type ButtonStyle =
|
|||
| "SUCCESS_BUTTON"
|
||||
| "DANGER_BUTTON";
|
||||
|
||||
export interface ButtonWidgetProps extends WidgetProps {
|
||||
export interface ButtonWidgetProps extends WidgetProps, WithMeta {
|
||||
text?: string;
|
||||
buttonStyle?: ButtonStyle;
|
||||
onClick?: string;
|
||||
isDisabled?: boolean;
|
||||
isVisible?: boolean;
|
||||
buttonType?: ButtonType;
|
||||
googleRecaptchaKey?: string;
|
||||
}
|
||||
|
||||
interface ButtonWidgetState extends WidgetState {
|
||||
|
|
@ -101,4 +126,4 @@ interface ButtonWidgetState extends WidgetState {
|
|||
}
|
||||
|
||||
export default ButtonWidget;
|
||||
export const ProfiledButtonWidget = Sentry.withProfiler(ButtonWidget);
|
||||
export const ProfiledButtonWidget = Sentry.withProfiler(withMeta(ButtonWidget));
|
||||
|
|
|
|||
|
|
@ -12,21 +12,30 @@ import {
|
|||
import { VALIDATION_TYPES } from "constants/WidgetValidation";
|
||||
import { TriggerPropertiesMap } from "utils/WidgetFactory";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import withMeta, { WithMeta } from "./MetaHOC";
|
||||
|
||||
class FormButtonWidget extends BaseWidget<
|
||||
FormButtonWidgetProps,
|
||||
FormButtonWidgetState
|
||||
> {
|
||||
onButtonClickBound: (event: React.MouseEvent<HTMLElement>) => void;
|
||||
clickWithRecaptchaBound: (token: string) => void;
|
||||
|
||||
constructor(props: FormButtonWidgetProps) {
|
||||
super(props);
|
||||
this.onButtonClickBound = this.onButtonClick.bind(this);
|
||||
this.clickWithRecaptchaBound = this.clickWithRecaptcha.bind(this);
|
||||
this.state = {
|
||||
isLoading: false,
|
||||
};
|
||||
}
|
||||
|
||||
static getMetaPropertiesMap(): Record<string, any> {
|
||||
return {
|
||||
recaptchaToken: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
static getPropertyValidationMap(): WidgetPropertyValidationType {
|
||||
return {
|
||||
...BASE_WIDGET_VALIDATION,
|
||||
|
|
@ -44,6 +53,21 @@ class FormButtonWidget extends BaseWidget<
|
|||
};
|
||||
}
|
||||
|
||||
clickWithRecaptcha(token: string) {
|
||||
if (this.props.onClick) {
|
||||
this.setState({
|
||||
isLoading: true,
|
||||
});
|
||||
}
|
||||
this.props.updateWidgetMetaProperty("recaptchaToken", token, {
|
||||
dynamicString: this.props.onClick,
|
||||
event: {
|
||||
type: EventType.ON_CLICK,
|
||||
callback: this.handleActionResult,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onButtonClick() {
|
||||
if (this.props.onClick) {
|
||||
this.setState({
|
||||
|
|
@ -88,6 +112,8 @@ class FormButtonWidget extends BaseWidget<
|
|||
onClick={this.onButtonClickBound}
|
||||
isLoading={this.props.isLoading || this.state.isLoading}
|
||||
type={this.props.buttonType || ButtonType.BUTTON}
|
||||
googleRecaptchaKey={this.props.googleRecaptchaKey}
|
||||
clickWithRecaptcha={this.clickWithRecaptchaBound}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -103,7 +129,7 @@ export type ButtonStyle =
|
|||
| "SUCCESS_BUTTON"
|
||||
| "DANGER_BUTTON";
|
||||
|
||||
export interface FormButtonWidgetProps extends WidgetProps {
|
||||
export interface FormButtonWidgetProps extends WidgetProps, WithMeta {
|
||||
text?: string;
|
||||
buttonStyle?: ButtonStyle;
|
||||
onClick?: string;
|
||||
|
|
@ -113,6 +139,7 @@ export interface FormButtonWidgetProps extends WidgetProps {
|
|||
resetFormOnClick?: boolean;
|
||||
onReset?: () => void;
|
||||
disabledWhenInvalid?: boolean;
|
||||
googleRecaptchaKey?: string;
|
||||
}
|
||||
|
||||
export interface FormButtonWidgetState extends WidgetState {
|
||||
|
|
@ -120,4 +147,6 @@ export interface FormButtonWidgetState extends WidgetState {
|
|||
}
|
||||
|
||||
export default FormButtonWidget;
|
||||
export const ProfiledFormButtonWidget = Sentry.withProfiler(FormButtonWidget);
|
||||
export const ProfiledFormButtonWidget = Sentry.withProfiler(
|
||||
withMeta(FormButtonWidget),
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user