Compare commits
8 Commits
2u-main
...
sundas/INF
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83d350f8c5 | ||
|
|
7a267e7200 | ||
|
|
6b5d4a9898 | ||
|
|
646b5d5f57 | ||
|
|
14eb973e13 | ||
|
|
23e6f0baf4 | ||
|
|
d427067f57 | ||
|
|
da0755467d |
1
.env
1
.env
@@ -46,3 +46,4 @@ APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
# Fallback in local style files
|
||||
PARAGON_THEME_URLS={}
|
||||
RECAPTCHA_SITE_KEY_WEB=''
|
||||
|
||||
@@ -46,3 +46,4 @@ ZENDESK_KEY=''
|
||||
ZENDESK_LOGO_URL=''
|
||||
# Fallback in local style files
|
||||
PARAGON_THEME_URLS={}
|
||||
RECAPTCHA_SITE_KEY_WEB=''
|
||||
|
||||
@@ -21,3 +21,4 @@ MFE_CONFIG_API_URL=''
|
||||
COHESION_WRITE_KEY=''
|
||||
COHESION_SOURCE_KEY=''
|
||||
PARAGON_THEME_URLS={}
|
||||
RECAPTCHA_SITE_KEY_WEB=''
|
||||
|
||||
13
package-lock.json
generated
13
package-lock.json
generated
@@ -32,6 +32,7 @@
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-error-boundary": "^4.0.13",
|
||||
"react-google-recaptcha-v3": "^1.11.0",
|
||||
"react-helmet": "6.1.0",
|
||||
"react-loading-skeleton": "3.5.0",
|
||||
"react-redux": "7.2.9",
|
||||
@@ -22541,6 +22542,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-google-recaptcha-v3": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/react-google-recaptcha-v3/-/react-google-recaptcha-v3-1.11.0.tgz",
|
||||
"integrity": "sha512-kLQqpz/77m8+trpBwzqcxNtvWZYoZ/YO6Vm2cVTHW8hs80BWUfDpC7RDwuAvpswwtSYApWfaSpIDFWAIBNIYxQ==",
|
||||
"dependencies": {
|
||||
"hoist-non-react-statics": "^3.3.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.3 || ^17.0 || ^18.0 || ^19.0",
|
||||
"react-dom": "^17.0 || ^18.0 || ^19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-helmet": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz",
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
"react-error-boundary": "^4.0.13",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-google-recaptcha-v3": "^1.11.0",
|
||||
"react-helmet": "6.1.0",
|
||||
"react-loading-skeleton": "3.5.0",
|
||||
"react-redux": "7.2.9",
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
|
||||
@@ -32,34 +33,43 @@ import './index.scss';
|
||||
|
||||
registerIcons();
|
||||
|
||||
const MainApp = () => (
|
||||
<AppProvider store={configureStore()}>
|
||||
<Helmet>
|
||||
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
|
||||
</Helmet>
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate replace to={updatePathWithQueryParams(REGISTER_PAGE)} />} />
|
||||
<Route
|
||||
path={REGISTER_EMBEDDED_PAGE}
|
||||
element={<EmbeddedRegistrationRoute><RegistrationPage /></EmbeddedRegistrationRoute>}
|
||||
/>
|
||||
<Route
|
||||
path={LOGIN_PAGE}
|
||||
element={
|
||||
<UnAuthOnlyRoute><Logistration selectedPage={LOGIN_PAGE} /></UnAuthOnlyRoute>
|
||||
}
|
||||
/>
|
||||
<Route path={REGISTER_PAGE} element={<UnAuthOnlyRoute><Logistration /></UnAuthOnlyRoute>} />
|
||||
<Route path={RESET_PAGE} element={<UnAuthOnlyRoute><ForgotPasswordPage /></UnAuthOnlyRoute>} />
|
||||
<Route path={PASSWORD_RESET_CONFIRM} element={<ResetPasswordPage />} />
|
||||
<Route path={AUTHN_PROGRESSIVE_PROFILING} element={<ProgressiveProfiling />} />
|
||||
<Route path={RECOMMENDATIONS} element={<RecommendationsPage />} />
|
||||
<Route path={PAGE_NOT_FOUND} element={<NotFoundPage />} />
|
||||
<Route path="*" element={<Navigate replace to={PAGE_NOT_FOUND} />} />
|
||||
</Routes>
|
||||
<RouteTracker />
|
||||
<MainAppSlot />
|
||||
</AppProvider>
|
||||
);
|
||||
const MainApp = () => {
|
||||
const recaptchaSiteKeyWeb = getConfig().RECAPTCHA_SITE_KEY_WEB;
|
||||
|
||||
return (
|
||||
<GoogleReCaptchaProvider
|
||||
reCaptchaKey={recaptchaSiteKeyWeb}
|
||||
useEnterprise
|
||||
>
|
||||
<AppProvider store={configureStore()}>
|
||||
<Helmet>
|
||||
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
|
||||
</Helmet>
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate replace to={updatePathWithQueryParams(REGISTER_PAGE)} />} />
|
||||
<Route
|
||||
path={REGISTER_EMBEDDED_PAGE}
|
||||
element={<EmbeddedRegistrationRoute><RegistrationPage /></EmbeddedRegistrationRoute>}
|
||||
/>
|
||||
<Route
|
||||
path={LOGIN_PAGE}
|
||||
element={
|
||||
<UnAuthOnlyRoute><Logistration selectedPage={LOGIN_PAGE} /></UnAuthOnlyRoute>
|
||||
}
|
||||
/>
|
||||
<Route path={REGISTER_PAGE} element={<UnAuthOnlyRoute><Logistration /></UnAuthOnlyRoute>} />
|
||||
<Route path={RESET_PAGE} element={<UnAuthOnlyRoute><ForgotPasswordPage /></UnAuthOnlyRoute>} />
|
||||
<Route path={PASSWORD_RESET_CONFIRM} element={<ResetPasswordPage />} />
|
||||
<Route path={AUTHN_PROGRESSIVE_PROFILING} element={<ProgressiveProfiling />} />
|
||||
<Route path={RECOMMENDATIONS} element={<RecommendationsPage />} />
|
||||
<Route path={PAGE_NOT_FOUND} element={<NotFoundPage />} />
|
||||
<Route path="*" element={<Navigate replace to={PAGE_NOT_FOUND} />} />
|
||||
</Routes>
|
||||
<RouteTracker />
|
||||
<MainAppSlot />
|
||||
</AppProvider>
|
||||
</GoogleReCaptchaProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainApp;
|
||||
|
||||
@@ -37,6 +37,7 @@ const configuration = {
|
||||
ALGOLIA_APP_ID: process.env.ALGOLIA_APP_ID || '',
|
||||
ALGOLIA_SEARCH_API_KEY: process.env.ALGOLIA_SEARCH_API_KEY || '',
|
||||
AUTO_GENERATED_USERNAME_EXPERIMENT_ID: process.env.AUTO_GENERATED_USERNAME_EXPERIMENT_ID || '',
|
||||
RECAPTCHA_SITE_KEY_WEB: process.env.RECAPTCHA_SITE_KEY_WEB || '',
|
||||
};
|
||||
|
||||
export default configuration;
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Form, Spinner, StatefulButton } from '@openedx/paragon';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useGoogleReCaptcha } from 'react-google-recaptcha-v3';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
@@ -60,6 +61,7 @@ import { trackRegistrationPageViewed, trackRegistrationSuccess } from '../tracki
|
||||
const RegistrationPage = (props) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const { executeRecaptcha } = useGoogleReCaptcha();
|
||||
|
||||
const registrationEmbedded = isHostAvailableInQueryParams();
|
||||
const platformName = getConfig().SITE_NAME;
|
||||
@@ -106,6 +108,8 @@ const RegistrationPage = (props) => {
|
||||
const [formStartTime, setFormStartTime] = useState(null);
|
||||
// temporary error state for embedded experience because we don't want to show errors on blur
|
||||
const [temporaryErrors, setTemporaryErrors] = useState({ ...backedUpFormData.errors });
|
||||
const [captchaError, setCaptchaError] = useState('');
|
||||
const intl = useIntl();
|
||||
|
||||
const { cta, host } = queryParams;
|
||||
const buttonLabel = cta
|
||||
@@ -230,7 +234,7 @@ const RegistrationPage = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const registerUser = () => {
|
||||
const registerUser = async () => {
|
||||
const totalRegistrationTime = (Date.now() - formStartTime) / 1000;
|
||||
let payload = { ...formFields, app_name: APP_NAME };
|
||||
|
||||
@@ -259,16 +263,30 @@ const RegistrationPage = (props) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Preparing payload for submission
|
||||
payload = prepareRegistrationPayload(
|
||||
payload,
|
||||
configurableFormFields,
|
||||
flags.showMarketingEmailOptInCheckbox,
|
||||
totalRegistrationTime,
|
||||
queryParams);
|
||||
if (executeRecaptcha) {
|
||||
const recaptchaToken = await executeRecaptcha('submit_post');
|
||||
if (!recaptchaToken) {
|
||||
setCaptchaError(intl.formatMessage(messages['discussions.captcha.verification.label']));
|
||||
return;
|
||||
}
|
||||
setCaptchaError('');
|
||||
// Preparing payload for submission
|
||||
if (recaptchaToken) {
|
||||
payload = prepareRegistrationPayload(
|
||||
payload,
|
||||
configurableFormFields,
|
||||
flags.showMarketingEmailOptInCheckbox,
|
||||
totalRegistrationTime,
|
||||
queryParams);
|
||||
|
||||
// making register call
|
||||
dispatch(registerNewUser(payload));
|
||||
const updatedpayload = {
|
||||
...payload,
|
||||
captcha_token: recaptchaToken,
|
||||
};
|
||||
// making register call
|
||||
dispatch(registerNewUser(updatedpayload));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
@@ -395,6 +413,11 @@ const RegistrationPage = (props) => {
|
||||
fieldDescriptions={fieldDescriptions}
|
||||
countriesCodesList={countriesCodesList}
|
||||
/>
|
||||
{captchaError && (
|
||||
<div className="mt-3 pgn__form-text-invalid pgn__form-text">
|
||||
{captchaError}
|
||||
</div>
|
||||
)}
|
||||
<StatefulButton
|
||||
id="register-user"
|
||||
name="register-user"
|
||||
|
||||
@@ -5,7 +5,10 @@ import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics'
|
||||
import {
|
||||
configure, getLocale, IntlProvider,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { fireEvent, render, waitFor } from '@testing-library/react';
|
||||
import {
|
||||
fireEvent, render, screen, waitFor,
|
||||
} from '@testing-library/react';
|
||||
import { useGoogleReCaptcha } from 'react-google-recaptcha-v3';
|
||||
import { mockNavigate, BrowserRouter as Router } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
@@ -25,6 +28,12 @@ import {
|
||||
APP_NAME, AUTHN_PROGRESSIVE_PROFILING, COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE,
|
||||
} from '../data/constants';
|
||||
|
||||
jest.mock('react-google-recaptcha-v3', () => ({
|
||||
useGoogleReCaptcha: jest.fn(),
|
||||
// eslint-disable-next-line react/prop-types
|
||||
GoogleReCaptchaProvider: ({ children }) => <div>{children}</div>,
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||
sendPageEvent: jest.fn(),
|
||||
sendTrackEvent: jest.fn(),
|
||||
@@ -133,7 +142,8 @@ describe('RegistrationPage', () => {
|
||||
institutionLogin: false,
|
||||
};
|
||||
window.location = { search: '' };
|
||||
useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
|
||||
const mockExecuteRecaptchaNew = jest.fn(() => Promise.resolve('mock-token'));
|
||||
useGoogleReCaptcha.mockReturnValue({ executeRecaptcha: mockExecuteRecaptchaNew });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -175,6 +185,35 @@ describe('RegistrationPage', () => {
|
||||
|
||||
// ******** test registration form submission ********
|
||||
|
||||
it('should show captcha error if executeRecaptcha returns null token', async () => {
|
||||
const mockExecuteRecaptcha = jest.fn().mockResolvedValue(null);
|
||||
useGoogleReCaptcha.mockReturnValue({ executeRecaptcha: mockExecuteRecaptcha });
|
||||
getLocale.mockImplementation(() => ('en-us'));
|
||||
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
|
||||
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL, search: '?next=/course/demo-course-url' };
|
||||
|
||||
const payload = {
|
||||
name: 'John Doe',
|
||||
username: 'john_doe',
|
||||
email: 'john.doe@gmail.com',
|
||||
password: 'password1',
|
||||
country: 'Pakistan',
|
||||
honor_code: true,
|
||||
total_registration_time: 0,
|
||||
next: '/course/demo-course-url',
|
||||
};
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
populateRequiredFields(getByLabelText, payload);
|
||||
const button = container.querySelector('button.btn-brand');
|
||||
fireEvent.click(button);
|
||||
|
||||
waitFor(() => expect(screen.getByText('CAPTCHA verification failed.')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('should submit form for valid input', () => {
|
||||
getLocale.mockImplementation(() => ('en-us'));
|
||||
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
|
||||
@@ -200,7 +239,7 @@ describe('RegistrationPage', () => {
|
||||
const button = container.querySelector('button.btn-brand');
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
|
||||
waitFor(() => { expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' })); });
|
||||
});
|
||||
|
||||
it('should submit form without password field when current provider is present', () => {
|
||||
@@ -233,7 +272,7 @@ describe('RegistrationPage', () => {
|
||||
populateRequiredFields(getByLabelText, formPayload, true);
|
||||
const button = container.querySelector('button.btn-brand');
|
||||
fireEvent.click(button);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...formPayload, country: 'PK' }));
|
||||
waitFor(() => { expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...formPayload, country: 'PK' })); });
|
||||
});
|
||||
|
||||
it('should display an error when form is submitted with an invalid email', () => {
|
||||
@@ -308,7 +347,7 @@ describe('RegistrationPage', () => {
|
||||
populateRequiredFields(getByLabelText, payload);
|
||||
const button = container.querySelector('button.btn-brand');
|
||||
fireEvent.click(button);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
|
||||
waitFor(() => { expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' })); });
|
||||
|
||||
mergeConfig({
|
||||
MARKETING_EMAILS_OPT_IN: '',
|
||||
@@ -335,7 +374,7 @@ describe('RegistrationPage', () => {
|
||||
populateRequiredFields(getByLabelText, payload, false, true);
|
||||
const button = container.querySelector('button.btn-brand');
|
||||
fireEvent.click(button);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
|
||||
waitFor(() => { expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' })); });
|
||||
mergeConfig({
|
||||
ENABLE_AUTO_GENERATED_USERNAME: false,
|
||||
});
|
||||
@@ -890,15 +929,16 @@ describe('RegistrationPage', () => {
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({
|
||||
name: 'John Doe',
|
||||
username: 'john_doe',
|
||||
email: 'john.doe@example.com',
|
||||
country: 'PK',
|
||||
social_auth_provider: 'Apple',
|
||||
total_registration_time: 0,
|
||||
app_name: APP_NAME,
|
||||
}));
|
||||
waitFor(() => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({
|
||||
name: 'John Doe',
|
||||
username: 'john_doe',
|
||||
email: 'john.doe@example.com',
|
||||
country: 'PK',
|
||||
social_auth_provider: 'Apple',
|
||||
total_registration_time: 0,
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,11 +4,11 @@ import { mergeConfig } from '@edx/frontend-platform';
|
||||
import {
|
||||
getLocale, IntlProvider,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { fireEvent, render, waitFor } from '@testing-library/react';
|
||||
import { useGoogleReCaptcha } from 'react-google-recaptcha-v3';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
import { APP_NAME } from '../../../data/constants';
|
||||
import { registerNewUser } from '../../data/actions';
|
||||
import { FIELDS } from '../../data/constants';
|
||||
import { NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
|
||||
@@ -26,6 +26,11 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
getLocale: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
|
||||
jest.mock('react-google-recaptcha-v3', () => ({
|
||||
useGoogleReCaptcha: jest.fn(),
|
||||
// eslint-disable-next-line react/prop-types
|
||||
GoogleReCaptchaProvider: ({ children }) => <div>{children}</div>,
|
||||
}));
|
||||
|
||||
const mockStore = configureStore();
|
||||
|
||||
@@ -220,6 +225,8 @@ describe('ConfigurableRegistrationForm', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const mockExecuteRecaptchaNew = jest.fn(() => Promise.resolve('mock-token'));
|
||||
useGoogleReCaptcha.mockReturnValue({ executeRecaptcha: mockExecuteRecaptchaNew });
|
||||
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
expect(document.querySelector('#profession')).toBeTruthy();
|
||||
expect(document.querySelector('#tos')).toBeTruthy();
|
||||
@@ -230,6 +237,8 @@ describe('ConfigurableRegistrationForm', () => {
|
||||
SHOW_CONFIGURABLE_EDX_FIELDS: true,
|
||||
});
|
||||
getLocale.mockImplementation(() => ('en-us'));
|
||||
const mockExecuteRecaptchaNew = jest.fn(() => Promise.resolve('mock-token'));
|
||||
useGoogleReCaptcha.mockReturnValue({ executeRecaptcha: mockExecuteRecaptchaNew });
|
||||
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
@@ -265,7 +274,9 @@ describe('ConfigurableRegistrationForm', () => {
|
||||
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK', app_name: APP_NAME }));
|
||||
waitFor(() => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
|
||||
});
|
||||
});
|
||||
|
||||
it('should show error messages for required fields on empty form submission', () => {
|
||||
|
||||
@@ -206,6 +206,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Did you mean',
|
||||
description: 'Did you mean alert suggestion',
|
||||
},
|
||||
'discussions.captcha.verification.label': {
|
||||
id: 'discussions.captcha.verification.label',
|
||||
defaultMessage: 'CAPTCHA verification failed.',
|
||||
description: 'CAPTCHA verification failed',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
Reference in New Issue
Block a user