Skip to content

Commit 0d9ff7c

Browse files
Merge pull request #612 from reportportal/develop
Release
2 parents 1e58f8e + 8fda915 commit 0d9ff7c

File tree

14 files changed

+240
-82
lines changed

14 files changed

+240
-82
lines changed

.github/workflows/deploy-dev.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ on:
3636
required: true
3737
CONTENTFUL_SPACE_ID_DEV:
3838
required: true
39+
RECAPTCHA_SITE_KEY:
40+
required: true
3941
inputs:
4042
CONTENTFUL_ENV_ID:
4143
description: 'Contentful environment to use'
@@ -106,6 +108,7 @@ jobs:
106108
echo CONTACT_US_URL=$CONTACT_US_URL >> .env.production
107109
echo DOCUMENTATION_URL=$DOCUMENTATION_URL >> .env.production
108110
echo GATSBY_MAILCHIMP_LIST_ID=$GATSBY_MAILCHIMP_LIST_ID >> .env.production
111+
echo RECAPTCHA_SITE_KEY=${{ secrets.RECAPTCHA_SITE_KEY }} >> .env.production
109112
110113
- name: Build the source code
111114
run: npm run build

.github/workflows/deploy-prod.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ jobs:
5959
echo CONTACT_US_URL=${{ env.CONTACT_US_URL }} >> .env.production
6060
echo DOCUMENTATION_URL=${{ env.DOCUMENTATION_URL }} >> .env.production
6161
echo GATSBY_MAILCHIMP_LIST_ID=${{ env.GATSBY_MAILCHIMP_LIST_ID }} >> .env.production
62+
echo RECAPTCHA_SITE_KEY=${{ secrets.RECAPTCHA_SITE_KEY }} >> .env.production
6263
6364
- name: Build the source code
6465
run: npm run build

src/components/Layout/Navigation/NavMenu/CommunityMenu/CommunityMenu.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export const CommunityMenu: FC<MenuProps> = ({ isDesktop = true, isOpen, menuCon
4242
);
4343

4444
const footer = (
45-
<div className={classNames(getBlocksWith('__footer'), 'temporary-hide')}>
45+
<div className={classNames(getBlocksWith('__footer'))}>
4646
<div className={getBlocksWith('__footer-container')}>
4747
<SubscriptionForm />
4848
</div>

src/components/SubscriptionBanner/SubscriptionBanner.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,6 @@ export const SubscriptionBanner: FC<PropsWithAnimation> = ({ isAnimationEnabled
2727
isAnimationEnabled,
2828
);
2929

30-
// temporary hidden
31-
return null;
32-
3330
return (
3431
<div ref={ref}>
3532
<FooterContent>

src/components/SubscriptionForm/SubscriptionForm.scss

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,12 @@
108108
text-decoration: underline;
109109
}
110110
}
111+
112+
.subscription-form__recaptcha-error {
113+
@include m.font-poppins();
114+
@include m.font-scale(small);
115+
116+
margin-bottom: 16px;
117+
color: var(--graphics-coral);
118+
}
111119
}

src/components/SubscriptionForm/SubscriptionForm.tsx

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import Icon from '@ant-design/icons';
33
import { Input, Form } from 'antd';
44
import { Link } from '@app/components/Link';
55
import { createBemBlockBuilder, EMAIL_VALIDATION_REGEX } from '@app/utils';
6+
import { useRecaptcha } from '@app/hooks/useRecaptcha';
67

78
import { EnvelopeIcon } from './icons';
89
import { SubscriptionFormCard } from './SubscriptionFormCard';
910
import { subscribeUser } from './utils';
1011

1112
import './SubscriptionForm.scss';
13+
import '../../containers/ContactUsPage/ContactUsPage.scss';
1214

1315
const getBlocksWith = createBemBlockBuilder(['subscription-form']);
1416

@@ -28,10 +30,12 @@ export const SubscriptionForm: FC = () => {
2830
}>({
2931
isValid: true,
3032
});
33+
const [isLoading, setIsLoading] = useState(false);
3134
const email = Form.useWatch('email', form);
35+
const { executeRecaptcha, recaptchaError, clearError } = useRecaptcha();
3236

33-
const handleSubscribeUser = (emailToSubscribe: string) => {
34-
subscribeUser(emailToSubscribe)
37+
const handleSubscribeUser = async (emailToSubscribe: string, recaptchaToken: string | null) => {
38+
return subscribeUser(emailToSubscribe, recaptchaToken)
3539
.then(response => {
3640
setValidation({
3741
isValid: true,
@@ -64,7 +68,7 @@ export const SubscriptionForm: FC = () => {
6468
});
6569
};
6670

67-
const handleFinish = () => {
71+
const handleFinish = async () => {
6872
const isLengthValid = email?.length <= 128;
6973
const isFormatValid = EMAIL_VALIDATION_REGEX.test(email);
7074

@@ -73,11 +77,32 @@ export const SubscriptionForm: FC = () => {
7377
isValid: false,
7478
message: 'Please use a valid email format',
7579
});
76-
7780
return;
7881
}
7982

80-
handleSubscribeUser(email);
83+
try {
84+
setIsLoading(true);
85+
clearError();
86+
87+
const recaptchaToken = await executeRecaptcha();
88+
89+
if (recaptchaError) {
90+
setValidation({
91+
isValid: false,
92+
message: 'Security verification failed. Please try again.',
93+
});
94+
return;
95+
}
96+
97+
await handleSubscribeUser(email, recaptchaToken);
98+
setIsLoading(false);
99+
} catch (error) {
100+
setIsLoading(false);
101+
setValidation({
102+
isValid: false,
103+
message: 'Subscription failed. Please try again.',
104+
});
105+
}
81106
};
82107

83108
useEffect(() => {
@@ -138,11 +163,12 @@ export const SubscriptionForm: FC = () => {
138163
<button
139164
type="submit"
140165
className="btn btn--primary"
141-
disabled={form.isFieldsTouched(true) && !validation.isValid}
166+
disabled={(form.isFieldsTouched(true) && !validation.isValid) || isLoading}
142167
>
143-
Subscribe
168+
{isLoading ? 'Subscribing...' : 'Subscribe'}
144169
</button>
145170
</Form.Item>
171+
{recaptchaError && <div className="recaptcha-error">{recaptchaError}</div>}
146172
<span className={getBlocksWith('__form-info')}>
147173
By subscribing, you agree to receive marketing emails from ReportPortal team and associated
148174
partners and accept our{' '}

src/components/SubscriptionForm/utils.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,11 @@ import axios, { AxiosPromise } from 'axios';
22

33
import { SUBSCRIPTION_URL } from './constants';
44

5-
export const subscribeUser = (email: string): AxiosPromise =>
6-
axios.post(SUBSCRIPTION_URL, { email_address: email });
5+
export const subscribeUser = (email: string, recaptchaToken: string | null): AxiosPromise => {
6+
const headers = {
7+
'Content-Type': 'application/json',
8+
...(recaptchaToken && { 'RP-Recaptcha-Token': recaptchaToken }),
9+
};
10+
11+
return axios.post(SUBSCRIPTION_URL, { email_address: email }, { headers });
12+
};

src/containers/ContactUsPage/ContactUsForm/ContactUsForm.tsx

Lines changed: 61 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import React, { useState, useRef, useEffect } from 'react';
1+
import React, { useState } from 'react';
22
import { FormikProvider, useFormik } from 'formik';
33
import { useBoolean } from 'ahooks';
44
import isEmpty from 'lodash/isEmpty';
55
import { Link } from '@app/components/Link';
6-
// import { subscribeUser } from '@app/components/SubscriptionForm/utils';
7-
import { createBemBlockBuilder } from '@app/utils';
6+
import { subscribeUser } from '@app/components/SubscriptionForm/utils';
7+
import { createBemBlockBuilder, CONTACT_US_URL } from '@app/utils';
8+
import { useRecaptcha } from '@app/hooks/useRecaptcha';
89
import axios from 'axios';
910

1011
import { validate, getBaseSalesForceValues } from './utils';
@@ -19,16 +20,11 @@ import '../ContactUsPage.scss';
1920

2021
const getBlocksWith = createBemBlockBuilder(['contact-us-form']);
2122

22-
const MIN_FORM_INTERACTION_TIME = 3000;
23-
2423
export const ContactUsForm = ({ title, options, isDiscussFieldShown }) => {
2524
const [isFeedbackFormVisible, { setTrue: showFeedbackForm }] = useBoolean(false);
2625
const [isLoading, setIsLoading] = useState(false);
27-
const formMountTimeRef = useRef<number | null>(null);
28-
29-
useEffect(() => {
30-
formMountTimeRef.current = Date.now();
31-
}, []);
26+
const [customError, setCustomError] = useState<string | null>(null);
27+
const { executeRecaptcha, recaptchaError, clearError } = useRecaptcha();
3228
const formik = useFormik({
3329
initialValues: {
3430
first_name: '',
@@ -37,54 +33,67 @@ export const ContactUsForm = ({ title, options, isDiscussFieldShown }) => {
3733
company: '',
3834
termsAgree: false,
3935
wouldLikeToReceiveAds: false,
40-
website: '', // Honeypot field - should remain empty
4136
...(isDiscussFieldShown && { discuss: '' }),
4237
},
4338
validateOnBlur: false,
4439
validateOnChange: false,
4540
validate,
4641
onSubmit: async values => {
47-
// Bot detection: Check honeypot field
48-
if (values.website) {
49-
console.warn('Bot detected: honeypot field filled');
42+
if (isLoading) {
43+
return;
44+
}
45+
46+
const errors = await validateForm();
47+
48+
if (!isEmpty(errors)) {
5049
return;
5150
}
5251

53-
// Bot detection: Check if form was submitted too quickly
54-
if (formMountTimeRef.current !== null) {
55-
const timeSinceMount = Date.now() - formMountTimeRef.current;
56-
if (timeSinceMount < MIN_FORM_INTERACTION_TIME) {
57-
console.warn('Bot detected: form submitted too quickly');
52+
try {
53+
setIsLoading(true);
54+
clearError();
55+
setCustomError(null);
56+
57+
const contactRecaptchaToken = await executeRecaptcha();
58+
if (!contactRecaptchaToken || recaptchaError) {
5859
return;
5960
}
60-
}
6161

62-
validateForm().then(errors => {
63-
if (isEmpty(errors)) {
64-
setIsLoading(true);
65-
66-
const baseSalesForceValues = getBaseSalesForceValues(options);
67-
// Remove honeypot field before submitting
68-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
69-
const { website, ...cleanValues } = values;
70-
const postData = {
71-
...cleanValues,
72-
...baseSalesForceValues,
73-
};
74-
75-
// if (values.wouldLikeToReceiveAds) {
76-
// subscribeUser(values.email).catch(console.error);
77-
// }
78-
79-
axios
80-
.post(`${process.env.CONTACT_US_URL}`, postData)
81-
.catch(console.error)
82-
.finally(() => {
83-
showFeedbackForm();
84-
setIsLoading(false);
85-
});
62+
const baseSalesForceValues = getBaseSalesForceValues(options);
63+
const postData = {
64+
...values,
65+
...baseSalesForceValues,
66+
};
67+
68+
if (values.wouldLikeToReceiveAds) {
69+
const subscribeRecaptchaToken = await executeRecaptcha();
70+
if (subscribeRecaptchaToken) {
71+
subscribeUser(values.email, subscribeRecaptchaToken).catch(console.error);
72+
}
73+
}
74+
75+
const headers = {
76+
'Content-Type': 'application/json',
77+
'RP-Recaptcha-Action': 'contact_us',
78+
...(contactRecaptchaToken && { 'RP-Recaptcha-Token': contactRecaptchaToken }),
79+
};
80+
81+
const response = await axios.post(CONTACT_US_URL, postData, { headers });
82+
83+
let responseData = response.data;
84+
if (typeof responseData === 'string') {
85+
responseData = JSON.parse(responseData);
86+
}
87+
88+
if (responseData.success) {
89+
showFeedbackForm();
90+
} else {
91+
setIsLoading(false);
8692
}
87-
});
93+
} catch (error) {
94+
setCustomError('Request failed. Please try again.');
95+
setIsLoading(false);
96+
}
8897
},
8998
});
9099
const { getFieldProps, validateForm } = formik;
@@ -107,20 +116,6 @@ export const ContactUsForm = ({ title, options, isDiscussFieldShown }) => {
107116
maxLength={80}
108117
/>
109118
<FormInput name="company" label="Company name" placeholder="ABC" maxLength={MAX_LENGTH} />
110-
{/* Honeypot field - hidden from users but visible to bots */}
111-
<div
112-
style={{ position: 'absolute', left: '-9999px', opacity: 0, pointerEvents: 'none' }}
113-
aria-hidden="true"
114-
>
115-
<FormInput
116-
name="website"
117-
label="Website"
118-
placeholder="https://example.com"
119-
maxLength={MAX_LENGTH}
120-
tabIndex={-1}
121-
autoComplete="off"
122-
/>
123-
</div>
124119
{isDiscussFieldShown && (
125120
<FormInput
126121
name="discuss"
@@ -130,9 +125,9 @@ export const ContactUsForm = ({ title, options, isDiscussFieldShown }) => {
130125
maxLength={MAX_LENGTH}
131126
/>
132127
)}
133-
{/* <FormFieldWrapper name="wouldLikeToReceiveAds"> */}
134-
{/* <CustomCheckbox label="Subscribe to ReportPortal newsletter" /> */}
135-
{/* </FormFieldWrapper> */}
128+
<FormFieldWrapper name="wouldLikeToReceiveAds">
129+
<CustomCheckbox label="Subscribe to ReportPortal newsletter" />
130+
</FormFieldWrapper>
136131
<FormFieldWrapper name="termsAgree">
137132
<CustomCheckbox
138133
label={
@@ -146,13 +141,16 @@ export const ContactUsForm = ({ title, options, isDiscussFieldShown }) => {
146141
}
147142
/>
148143
</FormFieldWrapper>
144+
{(recaptchaError || customError) && (
145+
<div className="recaptcha-error">{recaptchaError || customError}</div>
146+
)}
149147
<button
150148
className="btn btn--primary btn--large"
151149
type="submit"
152150
data-gtm="send_request"
153151
disabled={!getFieldProps('termsAgree').value || isLoading}
154152
>
155-
Send request
153+
{isLoading ? 'Sending...' : 'Send request'}
156154
</button>
157155
</form>
158156
</div>

src/containers/ContactUsPage/ContactUsForm/FormFieldWrapper/FormFieldWrapper.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ export interface BaseFieldProps {
99
maxLength?: number;
1010
value?: string;
1111
InputElement?: 'input' | 'textarea';
12-
tabIndex?: number;
13-
autoComplete?: string;
1412
}
1513

1614
export const FormFieldWrapper: FC<{ name: string; children: ReactElement }> = ({

src/containers/ContactUsPage/ContactUsForm/FormInput/FormInput.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import React, { FC } from 'react';
33
import { BaseFieldProps, FormFieldWrapper } from '../FormFieldWrapper';
44
import { InputField } from './InputField';
55

6-
export interface FormInputProps extends BaseFieldProps {
6+
interface FormInputProps extends BaseFieldProps {
77
type?: string;
88
}
99

0 commit comments

Comments
 (0)