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
|
|
@ -10,6 +10,12 @@ import { ButtonStyle } from "widgets/ButtonWidget";
|
||||||
import { Theme, darkenHover, darkenActive } from "constants/DefaultTheme";
|
import { Theme, darkenHover, darkenActive } from "constants/DefaultTheme";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { ComponentProps } from "components/designSystems/appsmith/BaseComponent";
|
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) => {
|
const getButtonColorStyles = (props: { theme: Theme } & ButtonStyleProps) => {
|
||||||
if (props.filled) return props.theme.colors.textOnDarkBG;
|
if (props.filled) return props.theme.colors.textOnDarkBG;
|
||||||
|
|
@ -124,6 +130,11 @@ export enum ButtonType {
|
||||||
BUTTON = "button",
|
BUTTON = "button",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RecaptchaProps {
|
||||||
|
googleRecaptchaKey?: string;
|
||||||
|
clickWithRecaptcha: (token: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
interface ButtonContainerProps extends ComponentProps {
|
interface ButtonContainerProps extends ComponentProps {
|
||||||
text?: string;
|
text?: string;
|
||||||
icon?: MaybeElement;
|
icon?: MaybeElement;
|
||||||
|
|
@ -148,20 +159,82 @@ const mapButtonStyleToStyleName = (buttonStyle?: ButtonStyle) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// To be used with the canvas
|
const RecaptchaComponent = (
|
||||||
const ButtonContainer = (props: ButtonContainerProps & ButtonStyleProps) => {
|
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 (
|
return (
|
||||||
<BaseButton
|
<div
|
||||||
loading={props.isLoading}
|
onClick={(event: React.MouseEvent<HTMLElement>) => {
|
||||||
icon={props.icon}
|
if (status === "ready") {
|
||||||
rightIcon={props.rightIcon}
|
(window as any).grecaptcha.ready(() => {
|
||||||
text={props.text}
|
try {
|
||||||
filled={props.buttonStyle !== "SECONDARY_BUTTON"}
|
(window as any).grecaptcha
|
||||||
accent={mapButtonStyleToStyleName(props.buttonStyle)}
|
.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}
|
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_TITLE = "Widgets";
|
||||||
export const WIDGET_SIDEBAR_CAPTION =
|
export const WIDGET_SIDEBAR_CAPTION =
|
||||||
"To add a widget, please drag and drop a widget on the canvas to the right";
|
"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",
|
controlType: "SWITCH",
|
||||||
isJSConvertible: true,
|
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",
|
helpText: "Disables clicks to this widget",
|
||||||
isJSConvertible: true,
|
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,
|
isVisible: isVisible,
|
||||||
text: "string",
|
text: "string",
|
||||||
isDisabled: "bool",
|
isDisabled: "bool",
|
||||||
|
recaptchaToken: "string",
|
||||||
|
googleRecaptchaKey: "string",
|
||||||
},
|
},
|
||||||
DATE_PICKER_WIDGET: {
|
DATE_PICKER_WIDGET: {
|
||||||
"!doc":
|
"!doc":
|
||||||
|
|
@ -176,6 +178,8 @@ export const entityDefinitions = {
|
||||||
isVisible: isVisible,
|
isVisible: isVisible,
|
||||||
text: "string",
|
text: "string",
|
||||||
isDisabled: "bool",
|
isDisabled: "bool",
|
||||||
|
recaptchaToken: "string",
|
||||||
|
googleRecaptchaKey: "string",
|
||||||
},
|
},
|
||||||
MAP_WIDGET: {
|
MAP_WIDGET: {
|
||||||
isVisible: isVisible,
|
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 { VALIDATION_TYPES } from "constants/WidgetValidation";
|
||||||
import { TriggerPropertiesMap } from "utils/WidgetFactory";
|
import { TriggerPropertiesMap } from "utils/WidgetFactory";
|
||||||
import * as Sentry from "@sentry/react";
|
import * as Sentry from "@sentry/react";
|
||||||
|
import withMeta, { WithMeta } from "./MetaHOC";
|
||||||
|
|
||||||
class ButtonWidget extends BaseWidget<ButtonWidgetProps, ButtonWidgetState> {
|
class ButtonWidget extends BaseWidget<ButtonWidgetProps, ButtonWidgetState> {
|
||||||
onButtonClickBound: (event: React.MouseEvent<HTMLElement>) => void;
|
onButtonClickBound: (event: React.MouseEvent<HTMLElement>) => void;
|
||||||
|
clickWithRecaptchaBound: (token: string) => void;
|
||||||
constructor(props: ButtonWidgetProps) {
|
constructor(props: ButtonWidgetProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.onButtonClickBound = this.onButtonClick.bind(this);
|
this.onButtonClickBound = this.onButtonClick.bind(this);
|
||||||
|
this.clickWithRecaptchaBound = this.clickWithRecaptcha.bind(this);
|
||||||
this.state = {
|
this.state = {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
};
|
};
|
||||||
|
|
@ -38,6 +40,11 @@ class ButtonWidget extends BaseWidget<ButtonWidgetProps, ButtonWidgetState> {
|
||||||
onClick: true,
|
onClick: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
static getMetaPropertiesMap(): Record<string, any> {
|
||||||
|
return {
|
||||||
|
recaptchaToken: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
onButtonClick() {
|
onButtonClick() {
|
||||||
if (this.props.onClick) {
|
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 = () => {
|
handleActionComplete = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
|
@ -72,6 +94,8 @@ class ButtonWidget extends BaseWidget<ButtonWidgetProps, ButtonWidgetState> {
|
||||||
onClick={this.onButtonClickBound}
|
onClick={this.onButtonClickBound}
|
||||||
isLoading={this.props.isLoading || this.state.isLoading}
|
isLoading={this.props.isLoading || this.state.isLoading}
|
||||||
type={this.props.buttonType || ButtonType.BUTTON}
|
type={this.props.buttonType || ButtonType.BUTTON}
|
||||||
|
googleRecaptchaKey={this.props.googleRecaptchaKey}
|
||||||
|
clickWithRecaptcha={this.clickWithRecaptchaBound}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -87,13 +111,14 @@ export type ButtonStyle =
|
||||||
| "SUCCESS_BUTTON"
|
| "SUCCESS_BUTTON"
|
||||||
| "DANGER_BUTTON";
|
| "DANGER_BUTTON";
|
||||||
|
|
||||||
export interface ButtonWidgetProps extends WidgetProps {
|
export interface ButtonWidgetProps extends WidgetProps, WithMeta {
|
||||||
text?: string;
|
text?: string;
|
||||||
buttonStyle?: ButtonStyle;
|
buttonStyle?: ButtonStyle;
|
||||||
onClick?: string;
|
onClick?: string;
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
isVisible?: boolean;
|
isVisible?: boolean;
|
||||||
buttonType?: ButtonType;
|
buttonType?: ButtonType;
|
||||||
|
googleRecaptchaKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ButtonWidgetState extends WidgetState {
|
interface ButtonWidgetState extends WidgetState {
|
||||||
|
|
@ -101,4 +126,4 @@ interface ButtonWidgetState extends WidgetState {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ButtonWidget;
|
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 { VALIDATION_TYPES } from "constants/WidgetValidation";
|
||||||
import { TriggerPropertiesMap } from "utils/WidgetFactory";
|
import { TriggerPropertiesMap } from "utils/WidgetFactory";
|
||||||
import * as Sentry from "@sentry/react";
|
import * as Sentry from "@sentry/react";
|
||||||
|
import withMeta, { WithMeta } from "./MetaHOC";
|
||||||
|
|
||||||
class FormButtonWidget extends BaseWidget<
|
class FormButtonWidget extends BaseWidget<
|
||||||
FormButtonWidgetProps,
|
FormButtonWidgetProps,
|
||||||
FormButtonWidgetState
|
FormButtonWidgetState
|
||||||
> {
|
> {
|
||||||
onButtonClickBound: (event: React.MouseEvent<HTMLElement>) => void;
|
onButtonClickBound: (event: React.MouseEvent<HTMLElement>) => void;
|
||||||
|
clickWithRecaptchaBound: (token: string) => void;
|
||||||
|
|
||||||
constructor(props: FormButtonWidgetProps) {
|
constructor(props: FormButtonWidgetProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.onButtonClickBound = this.onButtonClick.bind(this);
|
this.onButtonClickBound = this.onButtonClick.bind(this);
|
||||||
|
this.clickWithRecaptchaBound = this.clickWithRecaptcha.bind(this);
|
||||||
this.state = {
|
this.state = {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getMetaPropertiesMap(): Record<string, any> {
|
||||||
|
return {
|
||||||
|
recaptchaToken: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
static getPropertyValidationMap(): WidgetPropertyValidationType {
|
static getPropertyValidationMap(): WidgetPropertyValidationType {
|
||||||
return {
|
return {
|
||||||
...BASE_WIDGET_VALIDATION,
|
...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() {
|
onButtonClick() {
|
||||||
if (this.props.onClick) {
|
if (this.props.onClick) {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
|
@ -88,6 +112,8 @@ class FormButtonWidget extends BaseWidget<
|
||||||
onClick={this.onButtonClickBound}
|
onClick={this.onButtonClickBound}
|
||||||
isLoading={this.props.isLoading || this.state.isLoading}
|
isLoading={this.props.isLoading || this.state.isLoading}
|
||||||
type={this.props.buttonType || ButtonType.BUTTON}
|
type={this.props.buttonType || ButtonType.BUTTON}
|
||||||
|
googleRecaptchaKey={this.props.googleRecaptchaKey}
|
||||||
|
clickWithRecaptcha={this.clickWithRecaptchaBound}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -103,7 +129,7 @@ export type ButtonStyle =
|
||||||
| "SUCCESS_BUTTON"
|
| "SUCCESS_BUTTON"
|
||||||
| "DANGER_BUTTON";
|
| "DANGER_BUTTON";
|
||||||
|
|
||||||
export interface FormButtonWidgetProps extends WidgetProps {
|
export interface FormButtonWidgetProps extends WidgetProps, WithMeta {
|
||||||
text?: string;
|
text?: string;
|
||||||
buttonStyle?: ButtonStyle;
|
buttonStyle?: ButtonStyle;
|
||||||
onClick?: string;
|
onClick?: string;
|
||||||
|
|
@ -113,6 +139,7 @@ export interface FormButtonWidgetProps extends WidgetProps {
|
||||||
resetFormOnClick?: boolean;
|
resetFormOnClick?: boolean;
|
||||||
onReset?: () => void;
|
onReset?: () => void;
|
||||||
disabledWhenInvalid?: boolean;
|
disabledWhenInvalid?: boolean;
|
||||||
|
googleRecaptchaKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FormButtonWidgetState extends WidgetState {
|
export interface FormButtonWidgetState extends WidgetState {
|
||||||
|
|
@ -120,4 +147,6 @@ export interface FormButtonWidgetState extends WidgetState {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FormButtonWidget;
|
export default FormButtonWidget;
|
||||||
export const ProfiledFormButtonWidget = Sentry.withProfiler(FormButtonWidget);
|
export const ProfiledFormButtonWidget = Sentry.withProfiler(
|
||||||
|
withMeta(FormButtonWidget),
|
||||||
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user