From 5edcee9eb9d91d80ebf6db166b57dec372e746af Mon Sep 17 00:00:00 2001 From: Zainab Amir Date: Thu, 15 Jun 2023 15:46:34 +0500 Subject: [PATCH] feat: allow welcome page to load for embedded experience (#944) --- .env | 1 + src/common-components/UnAuthOnlyRoute.jsx | 2 +- src/common-components/data/reducers.js | 8 +- src/config/index.js | 1 + src/data/utils/index.js | 3 +- src/logistration/Logistration.jsx | 3 +- .../ProgressiveProfiling.jsx | 135 ++++++++++++------ src/progressive-profiling/data/reducers.js | 1 - src/progressive-profiling/data/selectors.js | 3 - src/progressive-profiling/index.js | 3 +- .../tests/ProgressiveProfiling.test.jsx | 87 ++++++----- src/register/RegistrationPage.jsx | 3 +- src/register/tests/RegistrationPage.test.jsx | 2 +- 13 files changed, 159 insertions(+), 93 deletions(-) delete mode 100644 src/progressive-profiling/data/selectors.js diff --git a/.env b/.env index d5b384e6..0bddb25b 100644 --- a/.env +++ b/.env @@ -19,6 +19,7 @@ REGISTER_CONVERSION_COOKIE_NAME=null # ***** Links ***** LOGIN_ISSUE_SUPPORT_LINK='' AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK=null +SEARCH_CATALOG_URL='' # ***** Features flags ***** DISABLE_ENTERPRISE_LOGIN='' ENABLE_DYNAMIC_REGISTRATION_FIELDS='' diff --git a/src/common-components/UnAuthOnlyRoute.jsx b/src/common-components/UnAuthOnlyRoute.jsx index 6208c9d2..11e20ab5 100644 --- a/src/common-components/UnAuthOnlyRoute.jsx +++ b/src/common-components/UnAuthOnlyRoute.jsx @@ -8,7 +8,7 @@ import { Route } from 'react-router-dom'; import { DEFAULT_REDIRECT_URL, REGISTER_PAGE, } from '../data/constants'; -import { isRegistrationEmbedded } from '../data/utils/dataUtils'; +import { isRegistrationEmbedded } from '../data/utils'; /** * This wrapper redirects the requester to our default redirect url if they are diff --git a/src/common-components/data/reducers.js b/src/common-components/data/reducers.js index 1bbeb338..7b449540 100644 --- a/src/common-components/data/reducers.js +++ b/src/common-components/data/reducers.js @@ -1,5 +1,5 @@ import { THIRD_PARTY_AUTH_CONTEXT, THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG } from './actions'; -import { COMPLETE_STATE, PENDING_STATE } from '../../data/constants'; +import { COMPLETE_STATE, FAILURE_STATE, PENDING_STATE } from '../../data/constants'; export const defaultState = { fieldDescriptions: {}, @@ -35,7 +35,11 @@ const reducer = (state = defaultState, action = {}) => { case THIRD_PARTY_AUTH_CONTEXT.FAILURE: return { ...state, - thirdPartyAuthApiStatus: COMPLETE_STATE, + thirdPartyAuthApiStatus: FAILURE_STATE, + thirdPartyAuthContext: { + ...state.thirdPartyAuthContext, + errorMessage: null, + }, }; case THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG: return { diff --git a/src/config/index.js b/src/config/index.js index c2a24d69..97278105 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -15,6 +15,7 @@ const configuration = { LOGIN_ISSUE_SUPPORT_LINK: process.env.LOGIN_ISSUE_SUPPORT_LINK || null, PASSWORD_RESET_SUPPORT_LINK: process.env.PASSWORD_RESET_SUPPORT_LINK || null, PRIVACY_POLICY: process.env.PRIVACY_POLICY || null, + SEARCH_CATALOG_URL: process.env.SEARCH_CATALOG_URL || null, TOS_AND_HONOR_CODE: process.env.TOS_AND_HONOR_CODE || null, TOS_LINK: process.env.TOS_LINK || null, // Miscellaneous diff --git a/src/data/utils/index.js b/src/data/utils/index.js index 8155b4d8..31725e2b 100644 --- a/src/data/utils/index.js +++ b/src/data/utils/index.js @@ -1,9 +1,10 @@ export { getTpaProvider, getTpaHint, - updatePathWithQueryParams, getAllPossibleQueryParams, getActivationStatus, + isRegistrationEmbedded, + updatePathWithQueryParams, windowScrollTo, } from './dataUtils'; export { default as AsyncActionType } from './reduxUtils'; diff --git a/src/logistration/Logistration.jsx b/src/logistration/Logistration.jsx index 27ea4827..d3b4f9a1 100644 --- a/src/logistration/Logistration.jsx +++ b/src/logistration/Logistration.jsx @@ -22,9 +22,8 @@ import { import messages from '../common-components/messages'; import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants'; import { - getTpaHint, getTpaProvider, updatePathWithQueryParams, + getTpaHint, getTpaProvider, isRegistrationEmbedded, updatePathWithQueryParams, } from '../data/utils'; -import { isRegistrationEmbedded } from '../data/utils/dataUtils'; import { LoginPage } from '../login'; import { RegistrationPage } from '../register'; import { backupRegistrationForm } from '../register/data/actions'; diff --git a/src/progressive-profiling/ProgressiveProfiling.jsx b/src/progressive-profiling/ProgressiveProfiling.jsx index ce4b5c37..44ef991e 100644 --- a/src/progressive-profiling/ProgressiveProfiling.jsx +++ b/src/progressive-profiling/ProgressiveProfiling.jsx @@ -23,15 +23,16 @@ import PropTypes from 'prop-types'; import { Helmet } from 'react-helmet'; import { saveUserProfile } from './data/actions'; -import { welcomePageSelector } from './data/selectors'; import messages from './messages'; import ProgressiveProfilingPageModal from './ProgressiveProfilingPageModal'; import { BaseComponent } from '../base-component'; import { RedirectLogistration } from '../common-components'; +import { getThirdPartyAuthContext } from '../common-components/data/actions'; +import { optionalFieldsSelector } from '../common-components/data/selectors'; import { - DEFAULT_REDIRECT_URL, DEFAULT_STATE, FAILURE_STATE, + DEFAULT_REDIRECT_URL, DEFAULT_STATE, FAILURE_STATE, PENDING_STATE, } from '../data/constants'; -import { getAllPossibleQueryParams } from '../data/utils'; +import { getAllPossibleQueryParams, isRegistrationEmbedded } from '../data/utils'; import { FormFieldRenderer } from '../field-renderer'; import { activateRecommendationsExperiment, RECOMMENDATIONS_EXP_VARIATION, trackRecommendationViewedOptimizely, @@ -39,47 +40,77 @@ import { import { trackRecommendationsGroup, trackRecommendationsViewed } from '../recommendations/track'; const ProgressiveProfiling = (props) => { - const { - formRenderState, submitState, showError, location, - } = props; - const enablePersonalizedRecommendations = getConfig().ENABLE_PERSONALIZED_RECOMMENDATIONS; - const registrationResponse = location.state?.registrationResult; - const { formatMessage } = useIntl(); - const [ready, setReady] = useState(false); - const [registrationResult, setRegistrationResult] = useState({ redirectUrl: '' }); - const [values, setValues] = useState({}); - const [openDialog, setOpenDialog] = useState(false); - const [showRecommendationsPage, setShowRecommendationsPage] = useState(false); + const { + getFieldDataFromBackend, + location, + submitState, + showError, + welcomePageContext, + welcomePageContextApiStatus, + } = props; + const registrationEmbedded = isRegistrationEmbedded(); + const authenticatedUser = getAuthenticatedUser(); const DASHBOARD_URL = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL); + const enablePersonalizedRecommendations = getConfig().ENABLE_PERSONALIZED_RECOMMENDATIONS; + + const [registrationResult, setRegistrationResult] = useState({ redirectUrl: '' }); + const [formFieldData, setFormFieldData] = useState({ fields: {}, extendedProfile: [] }); + const [canViewWelcomePage, setCanViewWelcomePage] = useState(false); + const [values, setValues] = useState({}); + const [showModal, setShowModal] = useState(false); + const [showRecommendationsPage, setShowRecommendationsPage] = useState(false); useEffect(() => { configureAuth(AxiosJwtAuthService, { loggingService: getLoggingService(), config: getConfig() }); ensureAuthenticatedUser(DASHBOARD_URL) .then(() => { hydrateAuthenticatedUser().then(() => { - setReady(true); + setCanViewWelcomePage(true); }); }) .catch(() => {}); - - if (registrationResponse) { - setRegistrationResult(registrationResponse); - } - }, [DASHBOARD_URL, registrationResponse]); + }, [DASHBOARD_URL]); useEffect(() => { - if (ready && authenticatedUser?.userId) { + const registrationResponse = location.state?.registrationResult; + if (registrationResponse) { + setRegistrationResult(registrationResponse); + setFormFieldData({ + fields: location.state?.optionalFields.fields, + extendedProfile: location.state?.optionalFields.extended_profile, + }); + } + }, [location.state]); + + useEffect(() => { + if (registrationEmbedded) { + getFieldDataFromBackend({ is_welcome_page: true }); + } + }, [registrationEmbedded, getFieldDataFromBackend]); + + useEffect(() => { + if (Object.keys(welcomePageContext).length !== 0) { + setFormFieldData({ + fields: welcomePageContext.fields, + extendedProfile: welcomePageContext.extended_profile, + }); + setRegistrationResult({ redirectUrl: getConfig().SEARCH_CATALOG_URL }); + } + }, [welcomePageContext]); + + useEffect(() => { + if (canViewWelcomePage && authenticatedUser?.userId) { identifyAuthenticatedUser(authenticatedUser.userId); sendPageEvent('login_and_registration', 'welcome'); } - }, [authenticatedUser, ready]); + }, [authenticatedUser, canViewWelcomePage]); useEffect(() => { - if (registrationResponse && authenticatedUser?.userId) { - const queryParams = getAllPossibleQueryParams(registrationResponse.redirectUrl); - if (enablePersonalizedRecommendations && !('enrollment_action' in queryParams)) { + if (registrationResult.redirectUrl && authenticatedUser?.userId) { + const redirectQueryParams = getAllPossibleQueryParams(registrationResult.redirectUrl); + if (enablePersonalizedRecommendations && !('enrollment_action' in redirectQueryParams)) { const userIdStr = authenticatedUser.userId.toString(); const variation = activateRecommendationsExperiment(userIdStr); const showRecommendations = variation === RECOMMENDATIONS_EXP_VARIATION; @@ -92,26 +123,26 @@ const ProgressiveProfiling = (props) => { } } } - }, [authenticatedUser, enablePersonalizedRecommendations, registrationResponse]); + }, [authenticatedUser, enablePersonalizedRecommendations, registrationResult]); - if (!location.state || !location.state.registrationResult || formRenderState === FAILURE_STATE) { + if ( + !(location.state?.registrationResult || registrationEmbedded) + || welcomePageContextApiStatus === FAILURE_STATE + ) { global.location.assign(DASHBOARD_URL); return null; } - if (!ready) { + if (!canViewWelcomePage) { return null; } - const optionalFields = location.state.optionalFields.fields; - const extendedProfile = location.state.optionalFields.extended_profile; - const handleSubmit = (e) => { e.preventDefault(); window.history.replaceState(location.state, null, ''); const payload = { ...values, extendedProfile: [] }; - if (Object.keys(extendedProfile).length > 0) { - extendedProfile.forEach(fieldName => { + if (Object.keys(formFieldData.extendedProfile).length > 0) { + formFieldData.extendedProfile.forEach(fieldName => { if (values[fieldName]) { payload.extendedProfile.push({ fieldName, fieldValue: values[fieldName] }); } @@ -133,7 +164,7 @@ const ProgressiveProfiling = (props) => { const handleSkip = (e) => { e.preventDefault(); window.history.replaceState(props.location.state, null, ''); - setOpenDialog(true); + setShowModal(true); sendTrackEvent('edx.bi.welcome.page.skip.link.clicked'); }; @@ -145,8 +176,8 @@ const ProgressiveProfiling = (props) => { } }; - const formFields = Object.keys(optionalFields).map((fieldName) => { - const fieldData = optionalFields[fieldName]; + const formFields = Object.keys(formFieldData.fields).map((fieldName) => { + const fieldData = formFieldData.fields[fieldName]; return ( { { siteName: getConfig().SITE_NAME })} - + {props.shouldRedirect ? ( { }; ProgressiveProfiling.propTypes = { - formRenderState: PropTypes.string.isRequired, location: PropTypes.shape({ state: PropTypes.shape({ registrationResult: PropTypes.shape({ @@ -245,10 +275,17 @@ ProgressiveProfiling.propTypes = { }), }), }), - saveUserProfile: PropTypes.func.isRequired, showError: PropTypes.bool, shouldRedirect: PropTypes.bool, submitState: PropTypes.string, + welcomePageContext: PropTypes.shape({ + extended_profile: PropTypes.arrayOf(PropTypes.string), + fields: PropTypes.shape({}), + }), + welcomePageContextApiStatus: PropTypes.string, + // Actions + getFieldDataFromBackend: PropTypes.func.isRequired, + saveUserProfile: PropTypes.func.isRequired, }; ProgressiveProfiling.defaultProps = { @@ -256,18 +293,26 @@ ProgressiveProfiling.defaultProps = { shouldRedirect: false, showError: false, submitState: DEFAULT_STATE, + welcomePageContext: {}, + welcomePageContextApiStatus: PENDING_STATE, }; -const mapStateToProps = state => ({ - formRenderState: welcomePageSelector(state).formRenderState, - shouldRedirect: welcomePageSelector(state).success, - submitState: welcomePageSelector(state).submitState, - showError: welcomePageSelector(state).showError, -}); +const mapStateToProps = state => { + const welcomePageStore = state.welcomePage; + + return { + shouldRedirect: welcomePageStore.success, + showError: welcomePageStore.showError, + submitState: welcomePageStore.submitState, + welcomePageContext: optionalFieldsSelector(state), + welcomePageContextApiStatus: state.commonComponents.thirdPartyAuthApiStatus, + }; +}; export default connect( mapStateToProps, { saveUserProfile, + getFieldDataFromBackend: getThirdPartyAuthContext, }, )(ProgressiveProfiling); diff --git a/src/progressive-profiling/data/reducers.js b/src/progressive-profiling/data/reducers.js index c087c070..4bd1eee7 100644 --- a/src/progressive-profiling/data/reducers.js +++ b/src/progressive-profiling/data/reducers.js @@ -6,7 +6,6 @@ import { export const defaultState = { extendedProfile: [], fieldDescriptions: {}, - formRenderState: DEFAULT_STATE, success: false, submitState: DEFAULT_STATE, showError: false, diff --git a/src/progressive-profiling/data/selectors.js b/src/progressive-profiling/data/selectors.js deleted file mode 100644 index 353ab13f..00000000 --- a/src/progressive-profiling/data/selectors.js +++ /dev/null @@ -1,3 +0,0 @@ -export const storeName = 'welcomePage'; - -export const welcomePageSelector = state => ({ ...state[storeName] }); diff --git a/src/progressive-profiling/index.js b/src/progressive-profiling/index.js index d44ed067..718f0cbb 100644 --- a/src/progressive-profiling/index.js +++ b/src/progressive-profiling/index.js @@ -1,4 +1,5 @@ +export const storeName = 'welcomePage'; + export { default as ProgressiveProfiling } from './ProgressiveProfiling'; export { default as reducer } from './data/reducers'; export { default as saga } from './data/sagas'; -export { storeName } from './data/selectors'; diff --git a/src/progressive-profiling/tests/ProgressiveProfiling.test.jsx b/src/progressive-profiling/tests/ProgressiveProfiling.test.jsx index a2f7d488..0e5bcca1 100644 --- a/src/progressive-profiling/tests/ProgressiveProfiling.test.jsx +++ b/src/progressive-profiling/tests/ProgressiveProfiling.test.jsx @@ -12,7 +12,7 @@ import { MemoryRouter, Router } from 'react-router-dom'; import configureStore from 'redux-mock-store'; import { - COMPLETE_STATE, DEFAULT_REDIRECT_URL, FAILURE_STATE, RECOMMENDATIONS, + COMPLETE_STATE, DEFAULT_REDIRECT_URL, EMBEDDED, FAILURE_STATE, RECOMMENDATIONS, } from '../../data/constants'; import { saveUserProfile } from '../data/actions'; import ProgressiveProfiling from '../ProgressiveProfiling'; @@ -34,6 +34,11 @@ jest.mock('@edx/frontend-platform/auth', () => ({ jest.mock('@edx/frontend-platform/logging', () => ({ getLoggingService: jest.fn(), })); +jest.mock('../../recommendations/optimizelyExperiment.js', () => ({ + activateRecommendationsExperiment: jest.fn().mockImplementation(() => 'welcome_page_recommendations_enabled'), + trackRecommendationViewedOptimizely: jest.fn(), + RECOMMENDATIONS_EXP_VARIATION: 'welcome_page_recommendations_enabled', +})); const history = createMemoryHistory(); @@ -57,8 +62,10 @@ describe('ProgressiveProfilingTests', () => { let store = {}; const DASHBOARD_URL = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL); const initialState = { - welcomePage: { - formRenderState: COMPLETE_STATE, + welcomePage: {}, + commonComponents: { + thirdPartyAuthApiStatus: null, + optionalFields: {}, }, }; @@ -96,7 +103,6 @@ describe('ProgressiveProfilingTests', () => { messages: { 'es-419': {}, de: {}, 'en-us': {} }, }); props = { - getFieldData: jest.fn(), location: { state: { registrationResult, @@ -106,7 +112,7 @@ describe('ProgressiveProfilingTests', () => { }; }); - it('not should display button "Learn more about how we use this information."', async () => { + it('should not display button "Learn more about how we use this information."', async () => { mergeConfig({ AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: '', }); @@ -124,11 +130,6 @@ describe('ProgressiveProfilingTests', () => { expect(progressiveProfilingPage.find('a.pgn__hyperlink').text()).toEqual('Learn more about how we use this information.'); }); - it('should render fields returned by backend api', async () => { - const progressiveProfilingPage = await getProgressiveProfilingPage(); - expect(progressiveProfilingPage.find('#gender').exists()).toBeTruthy(); - }); - it('should make identify call to segment on progressive profiling page', async () => { getAuthenticatedUser.mockReturnValue({ userId: 3, username: 'abc123' }); await getProgressiveProfilingPage(); @@ -168,6 +169,7 @@ describe('ProgressiveProfilingTests', () => { it('should show error message when patch request fails', async () => { store = mockStore({ + ...initialState, welcomePage: { ...initialState.welcomePage, showError: true, @@ -178,29 +180,14 @@ describe('ProgressiveProfilingTests', () => { expect(progressiveProfilingPage.find('#pp-page-errors').exists()).toBeTruthy(); }); - it('should redirect to dashboard if no form fields are configured', async () => { - store = mockStore({ - welcomePage: { - formRenderState: FAILURE_STATE, - }, - }); - - delete window.location; - window.location = { - href: getConfig().BASE_URL, - assign: jest.fn().mockImplementation((value) => { window.location.href = value; }), - }; - await getProgressiveProfilingPage(); - expect(window.location.href).toBe(DASHBOARD_URL); - }); - describe('Recommendations test', () => { mergeConfig({ ENABLE_PERSONALIZED_RECOMMENDATIONS: true, }); - it.skip('should redirect to recommendations page if recommendations are enabled', async () => { + it('should redirect to recommendations page if recommendations are enabled', async () => { store = mockStore({ + ...initialState, welcomePage: { ...initialState.welcomePage, success: true, @@ -215,15 +202,8 @@ describe('ProgressiveProfilingTests', () => { }); it('should not redirect to recommendations page if user is on its way to enroll in a course', async () => { - delete window.location; - window.location = { - href: getConfig().BASE_URL, - assign: jest.fn().mockImplementation((value) => { window.location.href = value; }), - }; - const redirectUrl = `${getConfig().LMS_BASE_URL}${DEFAULT_REDIRECT_URL}?enrollment_action=1`; props = { - getFieldData: jest.fn(), location: { state: { registrationResult: { @@ -236,6 +216,7 @@ describe('ProgressiveProfilingTests', () => { }; store = mockStore({ + ...initialState, welcomePage: { ...initialState.welcomePage, success: true, @@ -249,4 +230,42 @@ describe('ProgressiveProfilingTests', () => { expect(window.location.href).toEqual(redirectUrl); }); }); + + describe('Embedded Form Workflow Test', () => { + delete window.location; + window.location = { + assign: jest.fn().mockImplementation((value) => { window.location.href = value; }), + href: getConfig().BASE_URL, + search: `?variant=${EMBEDDED}`, + }; + + it('should render fields returned by backend API', async () => { + props = {}; + store = mockStore({ + ...initialState, + commonComponents: { + ...initialState.commonComponents, + thirdPartyAuthApiStatus: COMPLETE_STATE, + optionalFields, + }, + }); + + const progressiveProfilingPage = await getProgressiveProfilingPage(); + expect(progressiveProfilingPage.find('#gender').exists()).toBeTruthy(); + }); + + it('should redirect to dashboard if API call to get form field fails', async () => { + props = {}; + store = mockStore({ + ...initialState, + commonComponents: { + ...initialState.commonComponents, + thirdPartyAuthApiStatus: FAILURE_STATE, + }, + }); + + await getProgressiveProfilingPage(); + expect(window.location.href).toBe(DASHBOARD_URL); + }); + }); }); diff --git a/src/register/RegistrationPage.jsx b/src/register/RegistrationPage.jsx index 0f15d267..e5942117 100644 --- a/src/register/RegistrationPage.jsx +++ b/src/register/RegistrationPage.jsx @@ -51,9 +51,8 @@ import { INVALID_NAME_REGEX, LETTER_REGEX, NUMBER_REGEX, PENDING_STATE, REGISTER_PAGE, VALID_EMAIL_REGEX, } from '../data/constants'; import { - getAllPossibleQueryParams, getTpaHint, getTpaProvider, setCookie, + getAllPossibleQueryParams, getTpaHint, getTpaProvider, isRegistrationEmbedded, setCookie, } from '../data/utils'; -import { isRegistrationEmbedded } from '../data/utils/dataUtils'; const emailRegex = new RegExp(VALID_EMAIL_REGEX, 'i'); const urlRegex = new RegExp(INVALID_NAME_REGEX); diff --git a/src/register/tests/RegistrationPage.test.jsx b/src/register/tests/RegistrationPage.test.jsx index 3023a0fb..fe926897 100644 --- a/src/register/tests/RegistrationPage.test.jsx +++ b/src/register/tests/RegistrationPage.test.jsx @@ -822,7 +822,7 @@ describe('RegistrationPage', () => { }, commonComponents: { optionalFields: { - extended_profile: {}, + extended_profile: [], fields: { level_of_education: { name: 'level_of_education', error_message: false }, },