Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ yarn-error.log*

# misc
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
Expand Down
26 changes: 26 additions & 0 deletions package-lock.json

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

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
65 changes: 35 additions & 30 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -88,36 +90,39 @@ const App = () => {
<BreakpointProvider queries={defaultQuery}>
<BrowserRouter>
<Suspense fallback={<></>}>
<ToTop>
<Referrals />
<Announcement />
<Container>
<Header />

<Switch>
<Route exact path="/" render={props => <Redirect to={`/${lang}${props.location.search}`} />} />
<Route exact path="/:lang(en|de|ru)/terms-and-conditions" component={TermsConditions} />
<Route exact path="/:lang(en|de|ru)/privacy" component={Privacy} />
<Route exact path="/:lang(en|de|ru)/profile/:user?" component={Profile} />
<Route exact path="/:lang(en|de|ru)/order/:orderRef" component={Order} />
<Route exact path="/:lang(en|de|ru)/orders/:orderRef?" component={Orders} />
<Route exact path="/:lang(en|de|ru)" render={props => <Home {...props} store={store} />} />
<Route exact path="/:lang(en|de|ru)/instant-white-label/" component={WhiteLabelSEO} />
<Route exact path="/:lang(en|de|ru)/faqs/:id?" component={FAQ} />
<Route exact path="/:lang(en|de|ru)/about" component={About} />
<Route exact path="/:lang(en|de|ru)/signin" component={SignIn} />
<Route exact path="/:lang(en|de|ru)/signout" component={SignOut} />
<Route exact path="/:lang(en|de|ru)/signup" component={SignUp} />
<Route exact path="/:lang(en|de|ru)/forgot-password" component={ForgotPassword} />
<Route exact path="/:lang(en|de|ru)/convert/:quote-to-:base" render={props => <Pair {...props} store={store} />} />
<Route exact path="/:lang(en|de|ru)/not-found" component={NotFound} />
<Route component={NotFoundRedirect} />
</Switch>

<Footer />
</Container>
<Intercom />
</ToTop>
<GoogleReCaptchaProvider reCaptchaKey={process.env.REACT_APP_GOOGLE_RECAPTCHA_V3_SITE_KEY}>

<ToTop>
<Referrals />
<Announcement />
<Container>
<Header />

<Switch>
<Route exact path="/" render={props => <Redirect to={`/${lang}${props.location.search}`} />} />
<Route exact path="/:lang(en|de|ru)/terms-and-conditions" component={TermsConditions} />
<Route exact path="/:lang(en|de|ru)/privacy" component={Privacy} />
<Route exact path="/:lang(en|de|ru)/profile/:user?" component={Profile} />
<Route exact path="/:lang(en|de|ru)/order/:orderRef" component={Order} />
<Route exact path="/:lang(en|de|ru)/orders/:orderRef?" component={Orders} />
<Route exact path="/:lang(en|de|ru)" render={props => <Home {...props} store={store} />} />
<Route exact path="/:lang(en|de|ru)/instant-white-label/" component={WhiteLabelSEO} />
<Route exact path="/:lang(en|de|ru)/faqs/:id?" component={FAQ} />
<Route exact path="/:lang(en|de|ru)/about" component={About} />
<Route exact path="/:lang(en|de|ru)/signin" component={SignIn} />
<Route exact path="/:lang(en|de|ru)/signout" component={SignOut} />
<Route exact path="/:lang(en|de|ru)/signup" component={SignUp} />
<Route exact path="/:lang(en|de|ru)/forgot-password" component={ForgotPassword} />
<Route exact path="/:lang(en|de|ru)/convert/:quote-to-:base" render={props => <Pair {...props} store={store} />} />
<Route exact path="/:lang(en|de|ru)/not-found" component={NotFound} />
<Route component={NotFoundRedirect} />
</Switch>

<Footer />
</Container>
<Intercom />
</ToTop>
</GoogleReCaptchaProvider>
</Suspense>
</BrowserRouter>
</BreakpointProvider>
Expand Down
145 changes: 145 additions & 0 deletions src/components/Order/BotValidation/BotValidationProvider.js
Original file line number Diff line number Diff line change
@@ -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 (
<div className={`${styles.container} order-fiat`}>
<OrderLoading />
</div>
);
}

if (isAdditionalValidationNeeded) {
return (
<div className={`${styles.container} order-fiat`}>
<RecaptchaV2Container>
<ReCAPTCHA sitekey={process.env.REACT_APP_GOOGLE_RECAPTCHA_V2_SITE_KEY} onChange={validateWithUserInteraction} />
</RecaptchaV2Container>
</div>
);
}

if (isVerifiedHuman) {
return (
<>
<BotValidationContext.Provider
value={{
verifyIfHuman: validateSeamlessly,
isVerifiedHuman,

hookTriggerOnSuccess,
hookTriggerOnFailure,

isInitialLoading,
isVerificationInProgress,
}}
>
{children}
</BotValidationContext.Provider>
</>
);
}

return (
<div className={`${styles.container} order-fiat`}>
<OrderFailed title="error.notfound1" />
</div>
);

// endregion
};

export default BotValidationProvider;
23 changes: 23 additions & 0 deletions src/components/Order/BotValidation/recaptchaVerification.js
Original file line number Diff line number Diff line change
@@ -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);