Skip to content

Commit 6c4ae3c

Browse files
committed
feat: add Recaptcha
1 parent 4c3e7c7 commit 6c4ae3c

File tree

10 files changed

+121
-27
lines changed

10 files changed

+121
-27
lines changed

package-lock.json

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"prop-types": "^15.8.1",
5757
"react": "^18.3.1",
5858
"react-dom": "^18.3.1",
59+
"react-google-recaptcha-v3": "^1.11.0",
5960
"react-helmet": "^6.1.0",
6061
"react-hook-form": "^7.54.2",
6162
"react-intl": "^6.8.9",

src/components/FormFields/RegisterAccountFields.tsx

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Stack } from '@openedx/paragon';
33
import { Lock, Visibility, VisibilityOff } from '@openedx/paragon/icons';
44
import { useState } from 'react';
55

6-
import { useCountryOptions } from '@/components/app/data';
6+
import { useCountryOptions, useRecaptchaSubmission } from '@/components/app/data';
77
import { FieldContainer } from '@/components/FieldContainer';
88
import Field from '@/components/FormFields/Field';
99

@@ -230,18 +230,24 @@ export const RegisterAccountCountry = ({
230230
);
231231
};
232232

233-
const RegisterAccountFields = ({ form }: RegisterAccountFieldsProps) => (
234-
<FieldContainer>
235-
<RegisterAccountFieldsHeader />
236-
<Stack gap={3}>
237-
<RegisterAccountAdminEmail form={form} />
238-
<RegisterAccountFullName form={form} />
239-
<RegisterAccountUsername form={form} />
240-
<RegisterAccountPassword form={form} />
241-
<RegisterAccountConfirmPassword form={form} />
242-
<RegisterAccountCountry form={form} />
243-
</Stack>
244-
</FieldContainer>
245-
);
233+
const RegisterAccountFields = ({ form }: RegisterAccountFieldsProps) => {
234+
const { isLoading } = useRecaptchaSubmission('submit');
235+
if (isLoading) {
236+
return null;
237+
}
238+
return (
239+
<FieldContainer>
240+
<RegisterAccountFieldsHeader />
241+
<Stack gap={3}>
242+
<RegisterAccountAdminEmail form={form} />
243+
<RegisterAccountFullName form={form} />
244+
<RegisterAccountUsername form={form} />
245+
<RegisterAccountPassword form={form} />
246+
<RegisterAccountConfirmPassword form={form} />
247+
<RegisterAccountCountry form={form} />
248+
</Stack>
249+
</FieldContainer>
250+
);
251+
};
246252

247253
export default RegisterAccountFields;
Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
1+
import { getConfig } from '@edx/frontend-platform/config';
2+
import { ReactNode } from 'react';
3+
import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3';
4+
15
import PlanDetailsPage from '@/components/plan-details-pages/PlanDetailsPage';
26

3-
// TODO: unnecessary layer of abstraction, just move component logic into this file.
4-
const PlanDetails: React.FC = () => (
5-
<PlanDetailsPage />
6-
);
7+
const PlanDetails = (): ReactNode => {
8+
const { RECAPTCHA_SITE_WEB_KEY } = getConfig();
9+
return (
10+
<GoogleReCaptchaProvider
11+
reCaptchaKey={RECAPTCHA_SITE_WEB_KEY}
12+
useEnterprise
13+
>
14+
<PlanDetailsPage />
15+
</GoogleReCaptchaProvider>
16+
);
17+
};
718

819
export default PlanDetails;

src/components/app/data/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ export { default as useCreateCheckoutIntentMutation } from './useCreateCheckoutI
1313
export { default as useCheckoutSessionClientSecret } from './useCheckoutSessionClientSecret';
1414
export { default as useRegisterMutation } from './useRegisterMutation';
1515
export { default as useCountryOptions } from './useCountryOptions';
16+
export { default as useRecaptchaSubmission } from './useRecaptchaSubmission';
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { getConfig } from '@edx/frontend-platform';
2+
import { useCallback } from 'react';
3+
import { useGoogleReCaptcha } from 'react-google-recaptcha-v3';
4+
5+
const useRecaptchaSubmission = (actionName = 'submit') => {
6+
const { executeRecaptcha } = useGoogleReCaptcha();
7+
const { RECAPTCHA_SITE_WEB_KEY } = getConfig();
8+
9+
const isLoading = !(executeRecaptcha || RECAPTCHA_SITE_WEB_KEY);
10+
const isReady = !!executeRecaptcha && RECAPTCHA_SITE_WEB_KEY;
11+
12+
const executeWithFallback = useCallback(async () => {
13+
if (isReady) {
14+
const token = await executeRecaptcha(actionName);
15+
if (!token) {
16+
throw new Error("Oopsie! reCAPTCHA didn't return a token.");
17+
}
18+
return token;
19+
}
20+
21+
// Fallback: no reCAPTCHA or not ready
22+
if (RECAPTCHA_SITE_WEB_KEY) {
23+
// eslint-disable-next-line no-console
24+
console.warn(`reCAPTCHA not ready for action: ${actionName}. Proceeding without token.`);
25+
}
26+
return null;
27+
}, [isReady, RECAPTCHA_SITE_WEB_KEY, executeRecaptcha, actionName]);
28+
29+
return {
30+
executeWithFallback,
31+
isReady,
32+
isLoading,
33+
};
34+
};
35+
36+
export default useRecaptchaSubmission;

src/components/app/data/hooks/useRegisterMutation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export default function useRegisterMutation({
2222
return useMutation<
2323
AxiosResponse<RegistrationCreateSuccessResponseSchema>,
2424
AxiosError<RegistrationErrorResponseSchema>,
25-
RegistrationCreateRequestSchema
25+
Partial<RegistrationCreateRequestSchema>
2626
>({
2727
mutationFn: (requestData) => registerRequest(requestData),
2828
onSuccess: (axiosResponse) => onSuccess(axiosResponse.data),

src/components/app/data/services/registration.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,22 @@ declare global {
6262
/**
6363
* Account creation (register) request/response schemas.
6464
*/
65-
interface RegistrationCreateRequestSchema {
65+
interface BaseRegistrationCreateRequestSchema {
6666
name: string;
6767
username: string;
6868
password: string;
6969
email: string;
7070
country: string;
71-
honorCode?: boolean;
71+
}
72+
73+
interface RegistrationCreateRecaptchaRequestSchema extends BaseRegistrationCreateRequestSchema {
74+
recaptchaToken: string;
75+
}
76+
77+
interface RegistrationCreateRequestSchema extends
78+
BaseRegistrationCreateRequestSchema,
79+
RegistrationCreateRecaptchaRequestSchema {
80+
honorCode: boolean;
7281
}
7382

7483
interface RegistrationCreateSuccessResponseSchema {
@@ -387,7 +396,6 @@ export async function registerRequest(
387396
Object.entries(requestPayload as Record<string, unknown>).forEach(([key, value]) => {
388397
formParams.append(key, String(value));
389398
});
390-
391399
const response: AxiosResponse<RegistrationCreateSuccessResponseSchema> = (
392400
await getAuthenticatedHttpClient().post<RegistrationCreateSuccessResponsePayload>(
393401
`${getConfig().LMS_BASE_URL}/api/user/v2/account/registration/`,

src/components/plan-details-pages/PlanDetailsPage.tsx

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { FormattedMessage } from '@edx/frontend-platform/i18n';
2+
import { logError } from '@edx/frontend-platform/logging';
23
import { AppContext } from '@edx/frontend-platform/react';
34
import { zodResolver } from '@hookform/resolvers/zod';
45
import {
@@ -14,7 +15,7 @@ import { useForm } from 'react-hook-form';
1415
import { useLocation, useNavigate } from 'react-router-dom';
1516
import { z } from 'zod';
1617

17-
import { useFormValidationConstraints } from '@/components/app/data';
18+
import { useFormValidationConstraints, useRecaptchaSubmission } from '@/components/app/data';
1819
import {
1920
useCreateCheckoutIntentMutation,
2021
useLoginMutation,
@@ -42,6 +43,7 @@ import '../Stepper/Steps/css/PriceAlert.css';
4243
const PlanDetailsPage = () => {
4344
const location = useLocation();
4445
const queryClient = useQueryClient();
46+
4547
const { data: formValidationConstraints } = useFormValidationConstraints();
4648
const planDetailsFormData = useCheckoutFormStore((state) => state.formData[DataStoreKey.PlanDetails]);
4749
const setFormData = useCheckoutFormStore((state) => state.setFormData);
@@ -52,7 +54,7 @@ const PlanDetailsPage = () => {
5254
buttonMessage: stepperActionButtonMessage,
5355
formSchema,
5456
} = useCurrentPageDetails();
55-
57+
const { executeWithFallback } = useRecaptchaSubmission('submit');
5658
const planDetailsSchema = useMemo(() => (
5759
formSchema(formValidationConstraints, planDetailsFormData.stripePriceId)
5860
), [formSchema, formValidationConstraints, planDetailsFormData.stripePriceId]);
@@ -169,14 +171,29 @@ const PlanDetailsPage = () => {
169171
password: data.password,
170172
});
171173
},
172-
[SubmitCallbacks.PlanDetailsRegister]: (data: PlanDetailsRegisterPageData) => {
173-
registerMutation.mutate({
174+
[SubmitCallbacks.PlanDetailsRegister]: async (data: PlanDetailsRegisterPageData) => {
175+
let recaptchaToken: string | null = null;
176+
try {
177+
recaptchaToken = await executeWithFallback();
178+
} catch (err: any) {
179+
logError(err.message);
180+
}
181+
182+
let registerMutationPayload: BaseRegistrationCreateRequestSchema = {
174183
name: data.fullName,
175184
email: data.adminEmail,
176185
username: data.username,
177186
password: data.password,
178187
country: data.country,
179-
});
188+
};
189+
190+
if (recaptchaToken) {
191+
registerMutationPayload = {
192+
...registerMutationPayload,
193+
captchaToken: recaptchaToken,
194+
} as RegistrationCreateRecaptchaRequestSchema;
195+
}
196+
registerMutation.mutate(registerMutationPayload);
180197
},
181198
};
182199

src/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ initialize({
4949
ENTERPRISE_SALES_TERMS_AND_CONDITIONS_URL: process.env.ENTERPRISE_SALES_TERMS_AND_CONDITIONS_URL || null,
5050
COMPARE_ENTERPRISE_PLANS_URL: process.env.COMPARE_ENTERPRISE_PLANS_URL || null,
5151
CONTACT_SUPPORT_URL: process.env.CONTACT_SUPPORT_URL || null,
52+
RECAPTCHA_SITE_WEB_KEY: process.env.RECAPTCHA_SITE_WEB_KEY || null,
5253
});
5354
},
5455
},

0 commit comments

Comments
 (0)