Skip to content

Commit 711507c

Browse files
Merge branch 'release'
2 parents d789ac6 + 8bf80fe commit 711507c

File tree

8 files changed

+237
-17
lines changed

8 files changed

+237
-17
lines changed

app/client/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,4 +207,4 @@
207207
"pre-commit": "lint-staged"
208208
}
209209
}
210-
}
210+
}

app/client/src/components/designSystems/blueprint/ButtonComponent.tsx

Lines changed: 84 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ import { ButtonStyle } from "widgets/ButtonWidget";
1010
import { Theme, darkenHover, darkenActive } from "constants/DefaultTheme";
1111
import _ from "lodash";
1212
import { ComponentProps } from "components/designSystems/appsmith/BaseComponent";
13+
import useScript from "utils/hooks/useScript";
14+
import { AppToaster } from "components/editorComponents/ToastComponent";
15+
import {
16+
GOOGLE_RECAPTCHA_KEY_ERROR,
17+
GOOGLE_RECAPTCHA_DOMAIN_ERROR,
18+
} from "constants/messages";
1319

1420
const getButtonColorStyles = (props: { theme: Theme } & ButtonStyleProps) => {
1521
if (props.filled) return props.theme.colors.textOnDarkBG;
@@ -124,6 +130,11 @@ export enum ButtonType {
124130
BUTTON = "button",
125131
}
126132

133+
interface RecaptchaProps {
134+
googleRecaptchaKey?: string;
135+
clickWithRecaptcha: (token: string) => void;
136+
}
137+
127138
interface ButtonContainerProps extends ComponentProps {
128139
text?: string;
129140
icon?: MaybeElement;
@@ -148,20 +159,82 @@ const mapButtonStyleToStyleName = (buttonStyle?: ButtonStyle) => {
148159
}
149160
};
150161

162+
const RecaptchaComponent = (
163+
props: {
164+
children: any;
165+
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
166+
} & RecaptchaProps,
167+
) => {
168+
function handleError(event: React.MouseEvent<HTMLElement>, error: string) {
169+
AppToaster.show({
170+
message: error,
171+
type: "error",
172+
});
173+
props.onClick && props.onClick(event);
174+
}
175+
const status = useScript(
176+
`https://www.google.com/recaptcha/api.js?render=${props.googleRecaptchaKey}`,
177+
);
178+
return (
179+
<div
180+
onClick={(event: React.MouseEvent<HTMLElement>) => {
181+
if (status === "ready") {
182+
(window as any).grecaptcha.ready(() => {
183+
try {
184+
(window as any).grecaptcha
185+
.execute(props.googleRecaptchaKey, { action: "submit" })
186+
.then((token: any) => {
187+
props.clickWithRecaptcha(token);
188+
})
189+
.catch(() => {
190+
// Handle corrent key with wrong
191+
handleError(event, GOOGLE_RECAPTCHA_KEY_ERROR);
192+
});
193+
} catch (ex) {
194+
// Handle wrong key
195+
handleError(event, GOOGLE_RECAPTCHA_DOMAIN_ERROR);
196+
}
197+
});
198+
}
199+
}}
200+
>
201+
{props.children}
202+
</div>
203+
);
204+
};
205+
206+
const BtnWrapper = (
207+
props: {
208+
children: any;
209+
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
210+
} & RecaptchaProps,
211+
) => {
212+
if (!props.googleRecaptchaKey)
213+
return <div onClick={props.onClick}>{props.children}</div>;
214+
return <RecaptchaComponent {...props}></RecaptchaComponent>;
215+
};
216+
151217
// To be used with the canvas
152-
const ButtonContainer = (props: ButtonContainerProps & ButtonStyleProps) => {
218+
const ButtonContainer = (
219+
props: ButtonContainerProps & ButtonStyleProps & RecaptchaProps,
220+
) => {
153221
return (
154-
<BaseButton
155-
loading={props.isLoading}
156-
icon={props.icon}
157-
rightIcon={props.rightIcon}
158-
text={props.text}
159-
filled={props.buttonStyle !== "SECONDARY_BUTTON"}
160-
accent={mapButtonStyleToStyleName(props.buttonStyle)}
222+
<BtnWrapper
223+
googleRecaptchaKey={props.googleRecaptchaKey}
224+
clickWithRecaptcha={props.clickWithRecaptcha}
161225
onClick={props.onClick}
162-
disabled={props.disabled}
163-
type={props.type}
164-
/>
226+
>
227+
<BaseButton
228+
loading={props.isLoading}
229+
icon={props.icon}
230+
rightIcon={props.rightIcon}
231+
text={props.text}
232+
filled={props.buttonStyle !== "SECONDARY_BUTTON"}
233+
accent={mapButtonStyleToStyleName(props.buttonStyle)}
234+
disabled={props.disabled}
235+
type={props.type}
236+
/>
237+
</BtnWrapper>
165238
);
166239
};
167240

app/client/src/constants/messages.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,3 +163,7 @@ export const TABLE_FILTER_COLUMN_TYPE_CALLOUT =
163163
export const WIDGET_SIDEBAR_TITLE = "Widgets";
164164
export const WIDGET_SIDEBAR_CAPTION =
165165
"To add a widget, please drag and drop a widget on the canvas to the right";
166+
export const GOOGLE_RECAPTCHA_KEY_ERROR =
167+
"Google Re-Captcha Token Generation failed! Please check the Re-captcha Site Key.";
168+
export const GOOGLE_RECAPTCHA_DOMAIN_ERROR =
169+
"Google Re-Captcha Token Generation failed! Please check the allowed domains.";

app/client/src/mockResponses/PropertyPaneConfigResponse.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -806,6 +806,14 @@ const PropertyPaneConfigResponse: PropertyPaneConfigsResponse["data"] = {
806806
controlType: "SWITCH",
807807
isJSConvertible: true,
808808
},
809+
{
810+
id: "15.1.6",
811+
propertyName: "googleRecaptchaKey",
812+
label: "Google Recaptcha Key",
813+
helpText: "Sets Google Recaptcha v3 site key for button",
814+
controlType: "INPUT_TEXT",
815+
placeholderText: "Enter google recaptcha key",
816+
},
809817
],
810818
},
811819
{
@@ -954,6 +962,14 @@ const PropertyPaneConfigResponse: PropertyPaneConfigsResponse["data"] = {
954962
helpText: "Disables clicks to this widget",
955963
isJSConvertible: true,
956964
},
965+
{
966+
id: "1.1.4",
967+
propertyName: "googleRecaptchaKey",
968+
label: "Google Recaptcha Key",
969+
helpText: "Sets Google Recaptcha v3 site key for button",
970+
controlType: "INPUT_TEXT",
971+
placeholderText: "Enter google recaptcha key",
972+
},
957973
],
958974
},
959975
{

app/client/src/utils/autocomplete/EntityDefinitions.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ export const entityDefinitions = {
113113
isVisible: isVisible,
114114
text: "string",
115115
isDisabled: "bool",
116+
recaptchaToken: "string",
117+
googleRecaptchaKey: "string",
116118
},
117119
DATE_PICKER_WIDGET: {
118120
"!doc":
@@ -176,6 +178,8 @@ export const entityDefinitions = {
176178
isVisible: isVisible,
177179
text: "string",
178180
isDisabled: "bool",
181+
recaptchaToken: "string",
182+
googleRecaptchaKey: "string",
179183
},
180184
MAP_WIDGET: {
181185
isVisible: isVisible,
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { useState, useEffect } from "react";
2+
3+
// Hook
4+
export default function useScript(src: string) {
5+
// Keep track of script status ("idle", "loading", "ready", "error")
6+
const [status, setStatus] = useState(src ? "loading" : "idle");
7+
8+
useEffect(
9+
() => {
10+
// Allow falsy src value if waiting on other data needed for
11+
// constructing the script URL passed to this hook.
12+
if (!src) {
13+
setStatus("idle");
14+
return;
15+
}
16+
17+
// Fetch existing script element by src
18+
// It may have been added by another intance of this hook
19+
let script = document.querySelector(`script[src="${src}"]`) as any;
20+
21+
if (!script) {
22+
// Create script
23+
script = document.createElement("script");
24+
script.src = src;
25+
script.async = true;
26+
script.setAttribute("data-status", "loading");
27+
// Add script to document body
28+
document.body.appendChild(script);
29+
30+
// Store status in attribute on script
31+
// This can be read by other instances of this hook
32+
const setAttributeFromEvent = (event: any) => {
33+
script.setAttribute(
34+
"data-status",
35+
event.type === "load" ? "ready" : "error",
36+
);
37+
};
38+
39+
script.addEventListener("load", setAttributeFromEvent);
40+
script.addEventListener("error", setAttributeFromEvent);
41+
} else {
42+
// Grab existing script status from attribute and set to state.
43+
setStatus(script.getAttribute("data-status"));
44+
}
45+
46+
// Script event handler to update status in state
47+
// Note: Even if the script already exists we still need to add
48+
// event handlers to update the state for *this* hook instance.
49+
const setStateFromEvent = (event: any) => {
50+
setStatus(event.type === "load" ? "ready" : "error");
51+
};
52+
53+
// Add event listeners
54+
script.addEventListener("load", setStateFromEvent);
55+
script.addEventListener("error", setStateFromEvent);
56+
57+
// Remove event listeners on cleanup
58+
return () => {
59+
if (script) {
60+
script.removeEventListener("load", setStateFromEvent);
61+
script.removeEventListener("error", setStateFromEvent);
62+
}
63+
};
64+
},
65+
[src], // Only re-run effect if script src changes
66+
);
67+
68+
return status;
69+
}

app/client/src/widgets/ButtonWidget.tsx

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ import {
1212
import { VALIDATION_TYPES } from "constants/WidgetValidation";
1313
import { TriggerPropertiesMap } from "utils/WidgetFactory";
1414
import * as Sentry from "@sentry/react";
15+
import withMeta, { WithMeta } from "./MetaHOC";
1516

1617
class ButtonWidget extends BaseWidget<ButtonWidgetProps, ButtonWidgetState> {
1718
onButtonClickBound: (event: React.MouseEvent<HTMLElement>) => void;
18-
19+
clickWithRecaptchaBound: (token: string) => void;
1920
constructor(props: ButtonWidgetProps) {
2021
super(props);
2122
this.onButtonClickBound = this.onButtonClick.bind(this);
23+
this.clickWithRecaptchaBound = this.clickWithRecaptcha.bind(this);
2224
this.state = {
2325
isLoading: false,
2426
};
@@ -38,6 +40,11 @@ class ButtonWidget extends BaseWidget<ButtonWidgetProps, ButtonWidgetState> {
3840
onClick: true,
3941
};
4042
}
43+
static getMetaPropertiesMap(): Record<string, any> {
44+
return {
45+
recaptchaToken: undefined,
46+
};
47+
}
4148

4249
onButtonClick() {
4350
if (this.props.onClick) {
@@ -54,6 +61,21 @@ class ButtonWidget extends BaseWidget<ButtonWidgetProps, ButtonWidgetState> {
5461
}
5562
}
5663

64+
clickWithRecaptcha(token: string) {
65+
if (this.props.onClick) {
66+
this.setState({
67+
isLoading: true,
68+
});
69+
}
70+
this.props.updateWidgetMetaProperty("recaptchaToken", token, {
71+
dynamicString: this.props.onClick,
72+
event: {
73+
type: EventType.ON_CLICK,
74+
callback: this.handleActionComplete,
75+
},
76+
});
77+
}
78+
5779
handleActionComplete = () => {
5880
this.setState({
5981
isLoading: false,
@@ -72,6 +94,8 @@ class ButtonWidget extends BaseWidget<ButtonWidgetProps, ButtonWidgetState> {
7294
onClick={this.onButtonClickBound}
7395
isLoading={this.props.isLoading || this.state.isLoading}
7496
type={this.props.buttonType || ButtonType.BUTTON}
97+
googleRecaptchaKey={this.props.googleRecaptchaKey}
98+
clickWithRecaptcha={this.clickWithRecaptchaBound}
7599
/>
76100
);
77101
}
@@ -87,18 +111,19 @@ export type ButtonStyle =
87111
| "SUCCESS_BUTTON"
88112
| "DANGER_BUTTON";
89113

90-
export interface ButtonWidgetProps extends WidgetProps {
114+
export interface ButtonWidgetProps extends WidgetProps, WithMeta {
91115
text?: string;
92116
buttonStyle?: ButtonStyle;
93117
onClick?: string;
94118
isDisabled?: boolean;
95119
isVisible?: boolean;
96120
buttonType?: ButtonType;
121+
googleRecaptchaKey?: string;
97122
}
98123

99124
interface ButtonWidgetState extends WidgetState {
100125
isLoading: boolean;
101126
}
102127

103128
export default ButtonWidget;
104-
export const ProfiledButtonWidget = Sentry.withProfiler(ButtonWidget);
129+
export const ProfiledButtonWidget = Sentry.withProfiler(withMeta(ButtonWidget));

0 commit comments

Comments
 (0)