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.';