From 58032c131e0f117f92b90edf068db17b35afeac2 Mon Sep 17 00:00:00 2001
From: Mubbshar Anwar <78487564+mubbsharanwar@users.noreply.github.com>
Date: Mon, 24 Jun 2024 11:44:46 +0500
Subject: [PATCH] fix: progressive profiling and registration (#80)
- There should be no error message on undetected option in country field bydefault
- cookie value should be on variable base
- check user country on progressive profiling popup, if not set not allow user to skip or register
- pass country in register payload on the base of country cookie
---
.env.development | 1 +
example/index.jsx | 1 +
package-lock.json | 9 -----
package.json | 1 -
src/data/tests/utils.test.js | 27 ++++++++++++++-
src/data/utils.js | 6 ++++
.../progressive-profiling-popup/data/utils.js | 4 ---
.../progressive-profiling-popup/index.jsx | 27 ++++++++-------
.../index.test.jsx | 28 ++++++++++++++-
src/forms/registration-popup/index.jsx | 11 +++++-
.../tests/RegistrationPopup.test.jsx | 34 +++++++++++++++++++
11 files changed, 120 insertions(+), 29 deletions(-)
delete mode 100644 src/forms/progressive-profiling-popup/data/utils.js
diff --git a/.env.development b/.env.development
index 7f7e4949..09c1cc81 100644
--- a/.env.development
+++ b/.env.development
@@ -8,6 +8,7 @@ LOGOUT_URL=http://localhost:18000/login
REFRESH_ACCESS_TOKEN_ENDPOINT=http://localhost:18000/login_refresh
SITE_NAME=edX
INFO_EMAIL=info@edx.org
+ONBOARDING_COMPONENT_ENV='development'
# ***** Algolia keys to fetch subject list *****
AUTHN_ALGOLIA_APP_ID=''
AUTHN_ALGOLIA_SEARCH_API_KEY=''
diff --git a/example/index.jsx b/example/index.jsx
index bcb63a22..549fe96a 100644
--- a/example/index.jsx
+++ b/example/index.jsx
@@ -29,6 +29,7 @@ initialize({
PRIVACY_POLICY: process.env.PRIVACY_POLICY || '',
INFO_EMAIL: process.env.INFO_EMAIL || '',
USER_RETENTION_COOKIE_NAME: process.env.USER_RETENTION_COOKIE_NAME || '',
+ ONBOARDING_COMPONENT_ENV: process.env.ONBOARDING_COMPONENT_ENV || '',
});
},
},
diff --git a/package-lock.json b/package-lock.json
index f1b2b8ee..2ea731ab 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -26,7 +26,6 @@
"core-js": "3.36.0",
"fastest-levenshtein": "^1.0.16",
"form-urlencoded": "^6.1.5",
- "js-cookie": "^3.0.5",
"query-string": "^7.1.3",
"react-redux": "7.2.9",
"react-router": "6.22.3",
@@ -16316,14 +16315,6 @@
"integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==",
"peer": true
},
- "node_modules/js-cookie": {
- "version": "3.0.5",
- "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
- "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
- "engines": {
- "node": ">=14"
- }
- },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
diff --git a/package.json b/package.json
index 1d7198a2..fd8eb0f3 100644
--- a/package.json
+++ b/package.json
@@ -51,7 +51,6 @@
"core-js": "3.36.0",
"fastest-levenshtein": "^1.0.16",
"form-urlencoded": "^6.1.5",
- "js-cookie": "^3.0.5",
"query-string": "^7.1.3",
"react-redux": "7.2.9",
"react-router": "6.22.3",
diff --git a/src/data/tests/utils.test.js b/src/data/tests/utils.test.js
index d3293d2d..ceebf266 100644
--- a/src/data/tests/utils.test.js
+++ b/src/data/tests/utils.test.js
@@ -1,4 +1,7 @@
-import getAllPossibleQueryParams from '../utils';
+import { getConfig, mergeConfig } from '@edx/frontend-platform';
+import Cookies from 'universal-cookie';
+
+import getAllPossibleQueryParams, { getCountryCookieValue } from '../utils';
describe('getAllPossibleQueryParams', () => {
beforeEach(() => {
@@ -24,3 +27,25 @@ describe('getAllPossibleQueryParams', () => {
expect(queryParams).toEqual({ course_id: 'value1', next: 'value2' });
});
});
+
+describe('getCountryCookieValue', () => {
+ // Mock Cookies
+ jest.mock('universal-cookie');
+
+ mergeConfig({ ONBOARDING_COMPONENT_ENV: 'development' });
+
+ it('should return cookie value', () => {
+ const cookie = new Cookies();
+ cookie.set(`${getConfig().ONBOARDING_COMPONENT_ENV}-edx-cf-loc`, 'US');
+ const countryCode = getCountryCookieValue();
+ expect(countryCode).toEqual('US');
+ });
+
+ it('should return undefined cookie value', () => {
+ const cookie = new Cookies();
+ cookie.remove(`${getConfig().ONBOARDING_COMPONENT_ENV}-edx-cf-loc`);
+
+ const countryCode = getCountryCookieValue();
+ expect(countryCode).toEqual(undefined);
+ });
+});
diff --git a/src/data/utils.js b/src/data/utils.js
index 3681b9bc..106af1eb 100644
--- a/src/data/utils.js
+++ b/src/data/utils.js
@@ -47,4 +47,10 @@ export const setCookie = (cookieName, cookieValue, cookieExpiry) => {
}
};
+export const getCountryCookieValue = () => {
+ const cookieName = `${getConfig().ONBOARDING_COMPONENT_ENV}-edx-cf-loc`;
+ const cookies = new Cookies();
+ return cookies.get(cookieName);
+};
+
export default getAllPossibleQueryParams;
diff --git a/src/forms/progressive-profiling-popup/data/utils.js b/src/forms/progressive-profiling-popup/data/utils.js
deleted file mode 100644
index 6e1f160d..00000000
--- a/src/forms/progressive-profiling-popup/data/utils.js
+++ /dev/null
@@ -1,4 +0,0 @@
-import Cookies from 'js-cookie';
-
-const languageCookieValue = Cookies.get('prod-edx-language-preference');
-export default languageCookieValue;
diff --git a/src/forms/progressive-profiling-popup/index.jsx b/src/forms/progressive-profiling-popup/index.jsx
index b0da6c2d..dadd2997 100644
--- a/src/forms/progressive-profiling-popup/index.jsx
+++ b/src/forms/progressive-profiling-popup/index.jsx
@@ -14,11 +14,11 @@ import { Language } from '@openedx/paragon/icons';
import { extendedProfileFields, optionalFieldsData } from './data/constants';
import useSubjectsList from './data/hooks/useSubjectList';
import { saveUserProfile } from './data/reducers';
-import languageCookieValue from './data/utils';
import messages from './messages';
import { setCurrentOpenedForm } from '../../authn-component/data/reducers';
import { COMPLETE_STATE, LOGIN_FORM } from '../../data/constants';
import { useDispatch, useSelector } from '../../data/storeHooks';
+import { getCountryCookieValue } from '../../data/utils';
import {
trackProgressiveProfilingPageEvent,
trackProgressiveProfilingSkipLinkClickEvent,
@@ -39,15 +39,14 @@ const ProgressiveProfilingForm = () => {
const { formatMessage } = useIntl();
const dispatch = useDispatch();
+ const countryCookieValue = getCountryCookieValue();
const { subjectsList, subjectsLoading } = useSubjectsList();
const countryList = useMemo(() => getCountryList(getLocale()), []);
const submitState = useSelector(state => state.progressiveProfiling.submitState);
const redirectUrl = useSelector(state => state.progressiveProfiling.redirectUrl);
-
const authContextCountryCode = useSelector(state => state.commonData.thirdPartyAuthContext.countryCode);
const finishAuthUrl = useSelector(state => state.commonData.thirdPartyAuthContext.finishAuthUrl);
-
const authenticatedUser = useSelector(state => state.register.registrationResult.authenticatedUser);
const [formData, setFormData] = useState({});
@@ -56,14 +55,13 @@ const ProgressiveProfilingForm = () => {
useEffect(() => {
let countryCode = null;
- if (languageCookieValue) {
- countryCode = languageCookieValue;
- } else {
+ if (countryCookieValue) {
+ countryCode = countryCookieValue;
+ } else if (authContextCountryCode) {
countryCode = authContextCountryCode;
}
if (!countryCode) {
- setFormErrors({ country: formatMessage(messages.progressiveProfilingCountryFieldErrorMessage) });
return;
}
const userCountry = countryList.find((country) => country.code === countryCode);
@@ -72,7 +70,7 @@ const ProgressiveProfilingForm = () => {
// set formData state for auto populated country field to pass into payload
setFormData({ country: userCountry?.code });
}
- }, [authContextCountryCode, autoFilledCountry, countryList, formatMessage]);
+ }, [authContextCountryCode, autoFilledCountry, countryCookieValue, countryList, formatMessage]);
useEffect(() => {
if (authenticatedUser === null) {
@@ -159,11 +157,16 @@ const ProgressiveProfilingForm = () => {
const handleSkip = (e) => {
e.preventDefault();
- if (hasFormErrors()) {
- return;
+ const hasCountry = !!countryCookieValue || !!authContextCountryCode;
+ if (hasFormErrors() && !hasCountry) {
+ setFormErrors({ ...formErrors, country: formatMessage(messages.progressiveProfilingCountryFieldErrorMessage) });
+ } else if (!hasFormErrors() && !hasCountry) {
+ // TODO update error message copy here if user has no country value set in the backend and wants to skip this form
+ setFormErrors({ ...formErrors, country: formatMessage(messages.progressiveProfilingCountryFieldErrorMessage) });
+ } else if (hasCountry) {
+ trackProgressiveProfilingSkipLinkClickEvent();
+ window.location.href = redirectUrl;
}
- trackProgressiveProfilingSkipLinkClickEvent();
- window.location.href = redirectUrl;
};
return (
diff --git a/src/forms/progressive-profiling-popup/index.test.jsx b/src/forms/progressive-profiling-popup/index.test.jsx
index cf61d0b9..e9abae71 100644
--- a/src/forms/progressive-profiling-popup/index.test.jsx
+++ b/src/forms/progressive-profiling-popup/index.test.jsx
@@ -246,9 +246,14 @@ describe('ProgressiveProfilingForm Test', () => {
expect(countryInput.value).toEqual('United States of America');
});
- it('should redirect to redirect url on skip button click', () => {
+ it('should redirect to redirect url on skip button click if user profile has country field', () => {
store = mockStore({
...initialState,
+ commonData: {
+ thirdPartyAuthContext: {
+ countryCode: 'US',
+ },
+ },
progressiveProfiling: {
redirectUrl: 'http://example.com',
},
@@ -272,6 +277,27 @@ describe('ProgressiveProfilingForm Test', () => {
expect(window.location.href).toEqual('http://example.com');
});
+ it('should not redirect to redirect url on skip button click if country not set in user profile', () => {
+ store = mockStore({
+ ...initialState,
+ progressiveProfiling: {
+ redirectUrl: 'http://example.com',
+ },
+ });
+
+ delete window.location;
+ window.location = {
+ assign: jest.fn().mockImplementation((value) => { window.location.href = value; }),
+ href: getConfig().LMS_BASE_URL,
+ };
+ const { container } = render(reduxWrapper());
+
+ const skipButton = container.querySelector('#skip-optional-fields');
+ fireEvent.click(skipButton);
+
+ expect(window.location.href).not.toEqual('http://example.com');
+ });
+
it('should not redirect on skip button click if country not selected', () => {
store = mockStore({
...initialState,
diff --git a/src/forms/registration-popup/index.jsx b/src/forms/registration-popup/index.jsx
index 5fb1b93a..4704af59 100644
--- a/src/forms/registration-popup/index.jsx
+++ b/src/forms/registration-popup/index.jsx
@@ -32,7 +32,7 @@ import {
} from '../../data/constants';
import { useDispatch, useSelector } from '../../data/storeHooks';
import './index.scss';
-import getAllPossibleQueryParams, { setCookie } from '../../data/utils';
+import getAllPossibleQueryParams, { getCountryCookieValue, setCookie } from '../../data/utils';
import { registrationSuccessEvent, trackRegistrationPageEvent } from '../../tracking/trackers/register';
import AuthenticatedRedirection from '../common-components/AuthenticatedRedirection';
import SSOFailureAlert from '../common-components/SSOFailureAlert';
@@ -73,6 +73,7 @@ const RegistrationForm = () => {
const providers = useSelector(state => state.commonData.thirdPartyAuthContext?.providers);
const currentProvider = useSelector(state => state.commonData.thirdPartyAuthContext.currentProvider);
const pipelineUserDetails = useSelector(state => state.commonData.thirdPartyAuthContext.pipelineUserDetails);
+ const authContextCountryCode = useSelector(state => state.commonData.thirdPartyAuthContext.countryCode);
const registrationError = useSelector(state => state.register.registrationError);
const isLoginSSOIntent = useSelector(state => state.login.isLoginSSOIntent);
const registrationErrorCode = registrationError?.errorCode;
@@ -172,6 +173,7 @@ const RegistrationForm = () => {
};
const handleUserRegistration = () => {
+ const userCountryCode = getCountryCookieValue();
let payload = { ...formFields, honor_code: true, terms_of_service: true };
if (currentProvider) {
@@ -184,6 +186,13 @@ const RegistrationForm = () => {
}
}
+ // add country in payload if country cookie value or mfe_context country exists
+ if (userCountryCode) {
+ payload.country = userCountryCode;
+ } else if (authContextCountryCode) {
+ payload.country = authContextCountryCode;
+ }
+
// Validating form data before submitting
const { isValid, fieldErrors } = isFormValid(
payload,
diff --git a/src/forms/registration-popup/tests/RegistrationPopup.test.jsx b/src/forms/registration-popup/tests/RegistrationPopup.test.jsx
index 4a59e14c..1482aaa2 100644
--- a/src/forms/registration-popup/tests/RegistrationPopup.test.jsx
+++ b/src/forms/registration-popup/tests/RegistrationPopup.test.jsx
@@ -6,6 +6,7 @@ import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { fireEvent, render } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import configureStore from 'redux-mock-store';
+import Cookies from 'universal-cookie';
import { setCurrentOpenedForm } from '../../../authn-component/data/reducers';
import {
@@ -75,6 +76,7 @@ describe('RegistrationForm Test', () => {
TOS_AND_HONOR_CODE: process.env.TOS_AND_HONOR_CODE,
PRIVACY_POLICY: process.env.PRIVACY_POLICY,
USER_RETENTION_COOKIE_NAME: 'authn-returning-user',
+ ONBOARDING_COMPONENT_ENV: process.env.ONBOARDING_COMPONENT_ENV,
});
});
@@ -108,6 +110,38 @@ describe('RegistrationForm Test', () => {
expect(store.dispatch).toHaveBeenCalledWith(registerUser(payload));
});
+ it('should submit form with country code', async () => {
+ // Mock Cookies class
+ jest.mock('universal-cookie');
+
+ const cookie = new Cookies();
+ cookie.set(`${getConfig().ONBOARDING_COMPONENT_ENV}-edx-cf-loc`, 'US');
+
+ store.dispatch = jest.fn(store.dispatch);
+ const payload = {
+ email: 'test@example.com',
+ name: 'test',
+ password: 'test-password12',
+ marketing_email_opt_in: true,
+ honor_code: true,
+ terms_of_service: true,
+ country: 'US',
+ };
+ const { container } = render(reduxWrapper());
+
+ const emailInput = container.querySelector('#email');
+ fireEvent.change(emailInput, { target: { value: payload.email, name: 'email' } });
+
+ const nameInput = container.querySelector('#name');
+ const passwordInput = container.querySelector('#password');
+ const registerButton = container.querySelector('#register-user');
+ fireEvent.change(nameInput, { target: { value: payload.name, name: 'name' } });
+ fireEvent.change(passwordInput, { target: { value: payload.password, name: 'password' } });
+ fireEvent.click(registerButton);
+
+ expect(store.dispatch).toHaveBeenCalledWith(registerUser(payload));
+ });
+
it('should display an error when form is submitted with an invalid email', () => {
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
const emailError = 'We couldn’t create your account. Please correct the errors below.';