Skip to content

Commit

Permalink
feat/MSSDK-2049: captcha challenge in purchase flow (#447)
Browse files Browse the repository at this point in the history
* feat/MSSDK-2049: captcha challenge in purchase flow

* feat/MSSDK-2049: add missing prepare script

* feat/MSSDK-2049: fix deprecated husky command

* feat/MSSDK-2049: fix deprecated husky command

* feat/MSSDK-2049: add unit tests for the new recaptcha hook
  • Loading branch information
Saddage authored Jan 10, 2025
1 parent acb136c commit 3fef584
Show file tree
Hide file tree
Showing 10 changed files with 287 additions and 30 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"resolve": "1.22.0"
},
"scripts": {
"prepare": "husky",
"test": "vitest --silent --reporter=basic",
"test-ci": "vitest --run --silent --reporter=basic",
"clean": "rimraf dist",
Expand Down Expand Up @@ -107,6 +108,7 @@
"devDependencies": {
"@testing-library/jest-dom": "^6.1.5",
"@testing-library/react": "^13.4.0",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^14.4.3",
"@types/jest": "^29.5.12",
"@types/jwt-decode": "^3.1.0",
Expand Down
39 changes: 39 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/api/Payment/submitPayPalPayment.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { getData } from 'util/appConfigHelper';
import fetchWithJWT from 'util/fetchHelper';
import getApiURL from 'util/environmentHelper';

const submitPayPalPayment = async () => {
const submitPayPalPayment = async (captchaValue) => {
const API_URL = getApiURL();
const orderId = parseInt(getData('CLEENG_ORDER_ID') || '0', 10);
const url = `${API_URL}/connectors/paypal/v1/tokens`;
Expand All @@ -20,7 +20,7 @@ const submitPayPalPayment = async () => {
try {
const res = await fetchWithJWT(url, {
method: 'POST',
body: JSON.stringify({ orderId, ...redirectUrls })
body: JSON.stringify({ orderId, captchaValue, ...redirectUrls })
});
return res.json();
} catch (e) {
Expand Down
10 changes: 8 additions & 2 deletions src/api/Payment/submitPayment.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import getApiURL from 'util/environmentHelper';
import generateReturnUrl from 'util/returnUrlHelper';
import store from 'appRedux/store';

const submitPayment = async (paymentMethod, browserInfo, billingAddress) => {
const submitPayment = async ({
paymentMethod,
browserInfo,
billingAddress,
captchaValue
}) => {
const API_URL = getApiURL();

const orderId = parseInt(getData('CLEENG_ORDER_ID') || '0', 10);
Expand All @@ -24,7 +29,8 @@ const submitPayment = async (paymentMethod, browserInfo, billingAddress) => {
billingAddress,
origin: window.location.origin,
returnUrl: generateReturnUrl({ queryParams: { orderId } }),
enable3DSRedirectFlow
enable3DSRedirectFlow,
captchaValue
})
});
return res.json();
Expand Down
87 changes: 74 additions & 13 deletions src/components/Payment/Payment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import { fetchUpdateOrder, selectOnlyOrder } from 'appRedux/orderSlice';
import { setSelectedPaymentMethod } from 'appRedux/paymentMethodsSlice';
import { useAppDispatch, useAppSelector } from 'appRedux/store';
import RedirectElement from '@adyen/adyen-web';
import ReCAPTCHA from 'react-google-recaptcha';
import useCaptchaVerification from 'hooks/useCaptchaVerification';
import {
PaymentErrorStyled,
PaymentStyled,
Expand All @@ -43,30 +45,26 @@ import { PaymentProps } from './Payment.types';
const Payment = ({ onPaymentComplete }: PaymentProps) => {
const { paymentMethods: publisherPaymentMethods, isPayPalHidden } =
useAppSelector(selectPublisherConfig);

const order = useAppSelector(selectOnlyOrder);
const deliveryDetails = useAppSelector(selectDeliveryDetails);

const { t } = useTranslation();

const { requiredPaymentDetails: isPaymentDetailsRequired } = order;
const { loading: isPaymentFinalizationInProgress } = useAppSelector(
selectFinalizePayment
);
const { getCaptchaToken, recaptchaRef, showCaptchaOnPurchase, sitekey } =
useCaptchaVerification();
const dispatch = useAppDispatch();
const { t } = useTranslation();

const [isLoading, setIsLoading] = useState(false);

const [generalError, setGeneralError] = useState<string>('');
const [adyenKey, setAdyenKey] = useState<number | null>(null);

const [dropInInstance, setDropInInstance] = useState<
typeof RedirectElement | null
>(null);

const [isActionHandlingProcessing, setIsActionHandlingProcessing] =
useState(false);

const dispatch = useAppDispatch();
const { requiredPaymentDetails: isPaymentDetailsRequired } = order;

// order updates
const updateOrderWithPaymentMethodId = (methodId: number) => {
Expand Down Expand Up @@ -158,11 +156,47 @@ const Payment = ({ onPaymentComplete }: PaymentProps) => {
eventDispatcher(MSSDK_PAYMENT);
}, []);

const handleCaptchaVerification = async () => {
if (!showCaptchaOnPurchase) {
return {
shouldProceed: true,
captchaToken: ''
};
}

const {
recaptchaError: captchaError,
hasCaptchaSucceeded,
captchaToken
} = await getCaptchaToken();

if (!hasCaptchaSucceeded) {
setIsLoading(false);
setGeneralError(captchaError);

return {
shouldProceed: false,
captchaToken: ''
};
}

return {
captchaToken,
shouldProceed: true
};
};

// PayPal
const submitPayPal = async () => {
const { isGift } = deliveryDetails;
const { id, buyAsAGift } = order;

const { captchaToken, shouldProceed } = await handleCaptchaVerification();

if (!shouldProceed) {
return;
}

if (isGift) {
const areDeliveryDetailsValid = validateDeliveryDetailsForm();

Expand Down Expand Up @@ -211,7 +245,7 @@ const Payment = ({ onPaymentComplete }: PaymentProps) => {
}

setIsLoading(true);
const { responseData } = await submitPayPalPayment();
const { responseData } = await submitPayPalPayment(captchaToken);
if (responseData?.redirectUrl) {
window.location.href = responseData.redirectUrl;
} else {
Expand Down Expand Up @@ -250,11 +284,20 @@ const Payment = ({ onPaymentComplete }: PaymentProps) => {
} = state;
setGeneralError('');
setIsLoading(true);
const { errors, responseData } = await submitPayment(

const { captchaToken, shouldProceed } = await handleCaptchaVerification();

if (!shouldProceed) {
setIsLoading(false);
return;
}

const { errors, responseData } = await submitPayment({
paymentMethod,
browserInfo,
billingAddress
);
billingAddress,
captchaValue: captchaToken
});
if (errors?.length) {
eventDispatcher(MSSDK_PURCHASE_FAILED, {
reason: errors[0]
Expand Down Expand Up @@ -387,6 +430,15 @@ const Payment = ({ onPaymentComplete }: PaymentProps) => {
);
}

const handleCaptchaChange = () => {
if (
generalError ===
t('validators.captcha-invalid', 'Google reCAPTCHA verification required.')
) {
setGeneralError('');
}
};

return (
<PaymentStyled>
<SectionHeader marginTop='25px' paddingBottom='33px' center>
Expand Down Expand Up @@ -421,6 +473,15 @@ const Payment = ({ onPaymentComplete }: PaymentProps) => {
/>
</DropInSection>
)}
{showCaptchaOnPurchase && (
<ReCAPTCHA
ref={recaptchaRef}
size='invisible'
badge='bottomright'
sitekey={sitekey}
onChange={handleCaptchaChange}
/>
)}
{generalError && (
<PaymentErrorStyled>{generalError}</PaymentErrorStyled>
)}
Expand Down
24 changes: 12 additions & 12 deletions src/components/RegisterPage/useRegisterForm.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import React, { useState, useRef } from 'react';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import submitConsents from 'api/Customer/submitConsents';
import {
validateRegisterPassword,
validateEmailField,
validateConsentsField,
validateCaptcha
validateConsentsField
} from 'util/validators';
import { selectPublisherConfig } from 'appRedux/publisherConfigSlice';
import { selectPublisherConsents } from 'appRedux/publisherConsentsSlice';
Expand All @@ -14,9 +13,8 @@ import getCustomerLocales from 'api/Customer/getCustomerLocales';
import Auth from 'services/auth';
import { useAppSelector } from 'appRedux/store';
import { Consent as ConsentType } from 'types/Consents.types';
import ReCAPTCHA from 'react-google-recaptcha';
import ERROR_CODES from 'util/errorCodes';
import { normalizeCaptchaToken } from './utils';
import useCaptchaVerification from 'hooks/useCaptchaVerification';

type Errors = {
email: string;
Expand Down Expand Up @@ -47,6 +45,12 @@ function useRegisterForm({ onSuccess }: UseRegisterFormProps) {
[]
);
const [processing, setProcessing] = useState(false);
const {
recaptchaRef,
showCaptchaOnRegister,
getCaptchaToken,
validateCaptchaToken
} = useCaptchaVerification();

const { t } = useTranslation();
const { publisherId, googleRecaptcha } = useAppSelector(
Expand All @@ -56,10 +60,6 @@ function useRegisterForm({ onSuccess }: UseRegisterFormProps) {
selectPublisherConsents
);

const { showCaptchaOnRegister } = googleRecaptcha;

const recaptchaRef = useRef<ReCAPTCHA>(null);

const handleClickShowPassword = () =>
setShowPassword((prevValue) => !prevValue);

Expand Down Expand Up @@ -88,7 +88,7 @@ function useRegisterForm({ onSuccess }: UseRegisterFormProps) {
email: validateEmailField(email),
password: validateRegisterPassword(password),
consents: validateConsentsField(consents, consentDefinitions),
captcha: showCaptchaOnRegister ? validateCaptcha(captchaValue) : ''
captcha: showCaptchaOnRegister ? validateCaptchaToken(captchaValue) : ''
};
setErrors(errorFields);
return !Object.values(errorFields).some((error) => error !== '');
Expand Down Expand Up @@ -192,8 +192,8 @@ function useRegisterForm({ onSuccess }: UseRegisterFormProps) {
let captchaToken = '';

if (showCaptchaOnRegister) {
const fetchedCaptchaToken = await recaptchaRef?.current?.execute();
captchaToken = normalizeCaptchaToken(fetchedCaptchaToken);
const { captchaToken: fetchedCaptchaToken } = await getCaptchaToken();
captchaToken = fetchedCaptchaToken;
}

if (validateFields(captchaToken)) {
Expand Down
Loading

0 comments on commit 3fef584

Please sign in to comment.