diff --git a/.gitignore b/.gitignore
index bbe0b6388..dca29c4b9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -36,6 +36,7 @@ yarn-error.log*
# misc
.DS_Store
+.env
.env.local
.env.development.local
.env.test.local
diff --git a/package-lock.json b/package-lock.json
index 0f1f1187f..764352c09 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20994,6 +20994,15 @@
"@apollo/react-ssr": "^3.1.3"
}
},
+ "react-async-script": {
+ "version": "1.2.0",
+ "resolved": "https://artifacts.pwc.com/artifactory/api/npm/us-adv-digital-npm/react-async-script/-/react-async-script-1.2.0.tgz",
+ "integrity": "sha1-q5QSom8Lg/Xi4A3h0r78lACDSyE=",
+ "requires": {
+ "hoist-non-react-statics": "^3.3.0",
+ "prop-types": "^15.5.0"
+ }
+ },
"react-bootstrap": {
"version": "0.32.4",
"resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-0.32.4.tgz",
@@ -21316,6 +21325,23 @@
"use-sidecar": "^1.0.1"
}
},
+ "react-google-recaptcha": {
+ "version": "2.1.0",
+ "resolved": "https://artifacts.pwc.com/artifactory/api/npm/us-adv-digital-npm/react-google-recaptcha/-/react-google-recaptcha-2.1.0.tgz",
+ "integrity": "sha1-n29JVM5Jwd7avCxTI0cyHYktChY=",
+ "requires": {
+ "prop-types": "^15.5.0",
+ "react-async-script": "^1.1.1"
+ }
+ },
+ "react-google-recaptcha-v3": {
+ "version": "1.9.4",
+ "resolved": "https://artifacts.pwc.com/artifactory/api/npm/us-adv-digital-npm/react-google-recaptcha-v3/-/react-google-recaptcha-v3-1.9.4.tgz",
+ "integrity": "sha1-kSHYzaBIrm9gFNGYw/fznTKV6pA=",
+ "requires": {
+ "hoist-non-react-statics": "3.3.2"
+ }
+ },
"react-helmet": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-5.2.1.tgz",
diff --git a/package.json b/package.json
index 9e333fbc1..ca73fdfac 100644
--- a/package.json
+++ b/package.json
@@ -74,6 +74,8 @@
"react-dev-utils": "^6.0.0-next.3e165448",
"react-dom": "^16.8.6",
"react-fa": "^5.0.0",
+ "react-google-recaptcha": "^2.1.0",
+ "react-google-recaptcha-v3": "^1.9.4",
"react-helmet": "^5.2.1",
"react-i18next": "^7.7.0",
"react-id-swiper": "^1.6.8",
@@ -110,14 +112,14 @@
"whatwg-fetch": "2.0.3"
},
"devDependencies": {
- "@babel/plugin-proposal-optional-chaining": "^7.7.5",
- "babel-eslint": "^10.0.3",
"@apollo/react-testing": "^3.1.3",
+ "@babel/plugin-proposal-optional-chaining": "^7.7.5",
"@storybook/addon-actions": "^5.0.8",
"@storybook/addon-links": "^5.0.8",
"@storybook/addons": "^3.4.12",
"@storybook/react": "^5.0.8",
"axios-mock-adapter": "^1.15.0",
+ "babel-eslint": "^10.0.3",
"cypress": "^2.1.0",
"identity-obj-proxy": "^3.0.0",
"jest-localstorage-mock": "^2.2.0",
diff --git a/src/App.js b/src/App.js
index 3d45a3852..679dd7fbf 100644
--- a/src/App.js
+++ b/src/App.js
@@ -3,6 +3,8 @@ import { createStore, applyMiddleware, compose } from 'redux';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';
import { BrowserRouter, Route, Switch, Redirect, useLocation } from 'react-router-dom';
+import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3';
+
import styled from '@emotion/styled';
import './i18n';
@@ -88,36 +90,39 @@ const App = () => {
>}>
-
-
-
-
-
-
-
- } />
-
-
-
-
-
- } />
-
-
-
-
-
-
-
- } />
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+ } />
+
+
+
+
+
+ } />
+
+
+
+
+
+
+
+ } />
+
+
+
+
+
+
+
+
+
diff --git a/src/components/Order/BotValidation/BotValidationProvider.js b/src/components/Order/BotValidation/BotValidationProvider.js
new file mode 100644
index 000000000..e9f7a84a1
--- /dev/null
+++ b/src/components/Order/BotValidation/BotValidationProvider.js
@@ -0,0 +1,145 @@
+import React, { createContext, useEffect, useState } from 'react';
+import { useGoogleReCaptcha } from 'react-google-recaptcha-v3';
+import ReCAPTCHA from 'react-google-recaptcha';
+import styled from '@emotion/styled';
+
+import OrderFailed from '../OrderMain/OrderState/OrderFailure/OrderFailure';
+import OrderLoading from '../OrderLoading/OrderLoading';
+import { verifyRecaptchaV3IsHuman, verifyRecaptchaV2IsHuman } from './recaptchaVerification';
+
+import styles from '../Order.scss';
+
+const RecaptchaV2Container = styled.div`
+ width: 30%;
+ margin: auto;
+ padding: 5rem 0.5rem;
+`;
+
+export const BotValidationContext = createContext({});
+
+const BotValidationProvider = ({ children, orderId, initialActionName = 'initializing_bot_provider' }) => {
+ const { executeRecaptcha } = useGoogleReCaptcha();
+
+ // region States
+ const [isInitialLoading, setIsInitialLoading] = useState(true);
+
+ const [isVerifiedHuman, setIsVerifiedAsHuman] = useState(false);
+ const [isVerificationInProgress, setIsVerificationInProgress] = useState(false);
+ const [isAdditionalValidationNeeded, setIsAdditionalValidationNeeded] = useState(false);
+
+ const [hookTriggerOnSuccess, setHookTriggerOnSuccess] = useState(0);
+ const [hookTriggerOnFailure, setHookTriggerOnFailure] = useState(0);
+ // endregion
+
+ // region Trigger functions
+ const tickHookTriggerValidationOnSuccess = () => setHookTriggerOnSuccess(hookTriggerOnSuccess + 1);
+
+ const tickHookTriggerOnValidationFailure = () => setHookTriggerOnFailure(hookTriggerOnFailure + 1);
+ // endregion
+
+ const wrapWithAsyncLoadingState = validationLambdaFn => {
+ (async () => {
+ setIsVerificationInProgress(true);
+ await validationLambdaFn();
+ setIsVerificationInProgress(false);
+ })();
+ };
+
+ // region Seamless Validation
+ const verifyWithGoogleRecaptchaV3 = async actionName => {
+ const tokenV3 = await executeRecaptcha(actionName);
+ return verifyRecaptchaV3IsHuman(tokenV3, orderId);
+ };
+
+ const validateSeamlessly = (actionName = 'unkown_action') => {
+ wrapWithAsyncLoadingState(async () => {
+ const isHuman = await verifyWithGoogleRecaptchaV3(actionName);
+
+ if (isHuman) {
+ setIsVerifiedAsHuman(isHuman);
+ tickHookTriggerValidationOnSuccess();
+ } else {
+ // Enable validation with interaction
+ setIsAdditionalValidationNeeded(true);
+ }
+ });
+ };
+ // endregion
+
+ // region Validation With User Interaction
+ const verifyWithGoogleRecaptchaV2 = async tokenV2 => verifyRecaptchaV2IsHuman(tokenV2, orderId);
+
+ const validateWithUserInteraction = token => {
+ wrapWithAsyncLoadingState(async () => {
+ const isHuman = await verifyWithGoogleRecaptchaV2(token);
+ setIsVerifiedAsHuman(isHuman);
+ setIsAdditionalValidationNeeded(false);
+
+ if (isHuman) {
+ tickHookTriggerValidationOnSuccess();
+ } else {
+ tickHookTriggerOnValidationFailure();
+ }
+ });
+ };
+ // endregion
+
+ useEffect(() => {
+ if (executeRecaptcha) {
+ validateSeamlessly(initialActionName);
+ if (isInitialLoading) {
+ setIsInitialLoading(false);
+ }
+ }
+ }, [executeRecaptcha]);
+
+ // region Rendering
+ if (isInitialLoading || isVerificationInProgress) {
+ return (
+
+
+
+ );
+ }
+
+ if (isAdditionalValidationNeeded) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ if (isVerifiedHuman) {
+ return (
+ <>
+
+ {children}
+
+ >
+ );
+ }
+
+ return (
+
+
+
+ );
+
+ // endregion
+};
+
+export default BotValidationProvider;
diff --git a/src/components/Order/BotValidation/recaptchaVerification.js b/src/components/Order/BotValidation/recaptchaVerification.js
new file mode 100644
index 000000000..8a2a45d9a
--- /dev/null
+++ b/src/components/Order/BotValidation/recaptchaVerification.js
@@ -0,0 +1,23 @@
+import axios from 'axios';
+
+import config from '../../../config';
+
+const { API_BASE_URL } = config;
+
+export const verifyRecaptchaV3IsHuman = async token =>
+ axios
+ .post(`${API_BASE_URL}/recaptcha/v3`, { response_token: token })
+ .then(res => {
+ const { challenge_passed } = res.data;
+ return challenge_passed;
+ })
+ .catch(_ => false);
+
+export const verifyRecaptchaV2IsHuman = async token =>
+ axios
+ .post(`${API_BASE_URL}/recaptcha/v2`, { response_token: token })
+ .then(res => {
+ const { challenge_passed } = res.data;
+ return challenge_passed;
+ })
+ .catch(_ => false);