Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat/MSSDK-2049: captcha challenge in purchase flow #447

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading