feat: allow welcome page to load for embedded experience (#944)

This commit is contained in:
Zainab Amir
2023-06-15 15:46:34 +05:00
committed by GitHub
parent d41c06b1fd
commit 5edcee9eb9
13 changed files with 159 additions and 93 deletions

1
.env
View File

@@ -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=''

View File

@@ -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

View File

@@ -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 {

View File

@@ -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

View File

@@ -1,9 +1,10 @@
export {
getTpaProvider,
getTpaHint,
updatePathWithQueryParams,
getAllPossibleQueryParams,
getActivationStatus,
isRegistrationEmbedded,
updatePathWithQueryParams,
windowScrollTo,
} from './dataUtils';
export { default as AsyncActionType } from './reduxUtils';

View File

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

View File

@@ -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 (
<span key={fieldData.name}>
<FormFieldRenderer
@@ -165,7 +196,7 @@ const ProgressiveProfiling = (props) => {
{ siteName: getConfig().SITE_NAME })}
</title>
</Helmet>
<ProgressiveProfilingPageModal isOpen={openDialog} redirectUrl={registrationResult.redirectUrl} />
<ProgressiveProfilingPageModal isOpen={showModal} redirectUrl={registrationResult.redirectUrl} />
{props.shouldRedirect ? (
<RedirectLogistration
success
@@ -233,7 +264,6 @@ const ProgressiveProfiling = (props) => {
};
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);

View File

@@ -6,7 +6,6 @@ import {
export const defaultState = {
extendedProfile: [],
fieldDescriptions: {},
formRenderState: DEFAULT_STATE,
success: false,
submitState: DEFAULT_STATE,
showError: false,

View File

@@ -1,3 +0,0 @@
export const storeName = 'welcomePage';
export const welcomePageSelector = state => ({ ...state[storeName] });

View File

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

View File

@@ -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);
});
});
});

View File

@@ -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);

View File

@@ -822,7 +822,7 @@ describe('RegistrationPage', () => {
},
commonComponents: {
optionalFields: {
extended_profile: {},
extended_profile: [],
fields: {
level_of_education: { name: 'level_of_education', error_message: false },
},