Compare commits

..

24 Commits

Author SHA1 Message Date
mubbsharanwar
74fc221674 feat: onboarding component recommendations on authn 2024-07-24 15:57:10 +05:00
Mubbshar Anwar
6b983e18d3 fix: remove cookie (#1286)
-remove marketingEmailsOptIn cookie on successful registration
- fix tests
2024-07-12 17:05:50 +05:00
Syed Sajjad Hussain Shah
327210192c fix: set marketing opt in in cookie for sso (#1285) 2024-07-12 13:18:42 +05:00
Muhammad Abdullah Waheed
0d603b5fa1 feat: added app name identifier in segment events (#1277)
* feat: added app name identifier in registration call

* feat: added utils for tracking events

* refactor: mapped login events

* refactor: mapped forgot password events

* refactor: mapped reset password events

* refactor: mapped register events

* fix: fixed unit tests

* refactor: mapped progressive prifiling events

* fix: fixed unit tests

* refactor: added app name in logistration events

* refactor: resolved PR reviews and fixed tests
2024-07-03 17:08:44 +05:00
Mubbshar Anwar
efaa83a1bc feat: hard code fields on frontend (#1256)
* feat: hard code fields
hard code configurable fields on frontend which includes country field on register page & level of education & gender field on progressive profiling

VAN-1971

* fix: fix secondary provider null name issue
2024-06-11 12:01:30 +05:00
Mubbshar Anwar
bd63bb1f15 Update 2u-main with master (#1254)
* feat: Hide preloaders for third party auth providers if they are disabled

* feat: remove username from the registration from (#1201) (#1241)

Co-authored-by: Attiya Ishaque <atiya.ishaq@arbisoft.com>

* fix: add new entry for another US label (#1244)

Add new entry for for another US label which is United States

* feat: implement multi step registration experiment

Rebase 2u main with master (#1228)

* chore(deps): update dependency babel-plugin-formatjs to v10.5.14

* fix(deps): update dependency @edx/frontend-platform to v7.1.3

* fix(deps): update font awesome to v6.5.2

* chore(deps): update dependency @openedx/frontend-build to v13.1.4

* fix(deps): update dependency @openedx/paragon to v22.2.1

* fix(deps): update dependency algoliasearch to v4.23.3

* fix(deps): update dependency algoliasearch-helper to v3.17.0

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* feat: add multi step registration eventing (#1226)

* feat: implement multi step registration experiment

* feat: add multi step registration eventing

* fix: fix register button width

* fix: fix register button loader for control

* feat: capture marketing lead in experiment events (#1243)

* revert: multistep registration experiment
revert multistep registration experiment changes

VAN-1930

* feat: implement auto generated username experiment (#1248)

* feat: implement auto generated username registration exp

* feat: add page event for reset password (#1253)

Description: Add page event for reset password page
VAN-1929

---------

Co-authored-by: Stanislav Lunyachek <stanislav.lunyachek@raccoongang.com>
Co-authored-by: Syed Sajjad Hussain Shah <52817156+syedsajjadkazmii@users.noreply.github.com>
Co-authored-by: Attiya Ishaque <atiya.ishaq@arbisoft.com>
Co-authored-by: Blue <ahtesham-quraish@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Syed Sajjad  Hussain Shah <ssajjad@2u.com>
2024-06-07 07:57:42 +05:00
Blue
5754c2961a feat: add page event for reset password (#1253)
Description: Add page event for reset password page
VAN-1929
2024-06-04 16:27:14 +05:00
Blue
dcbd644a25 feat: implement auto generated username experiment (#1248)
* feat: implement auto generated username registration exp
2024-05-13 14:11:03 +05:00
Blue
52e438652c 2u-main rebase with master (#1246)
Rebase 2u-main with master
2024-05-07 16:44:47 +05:00
mubbsharanwar
d8947a4c0a revert: multistep registration experiment
revert multistep registration experiment changes

VAN-1930
2024-05-07 11:47:28 +05:00
Syed Sajjad Hussain Shah
03d1666c2c feat: capture marketing lead in experiment events (#1243) 2024-04-25 15:28:05 +05:00
Syed Sajjad Hussain Shah
3782503983 fix: fix register button loader for control 2024-04-22 16:53:23 +05:00
Syed Sajjad Hussain Shah
b219fe3683 fix: fix register button width 2024-04-22 14:37:36 +05:00
Syed Sajjad Hussain Shah
90f650ce3e feat: add multi step registration eventing (#1226)
* feat: implement multi step registration experiment

* feat: add multi step registration eventing
2024-04-18 11:09:32 +05:00
Syed Sajjad Hussain Shah
6f325c20c3 feat: implement multi step registration experiment 2024-04-18 11:09:32 +05:00
Syed Sajjad Hussain Shah
de12dfbf9e Merge pull request #1236 from openedx/master
adding master commits to 2u-main
2024-04-18 10:19:28 +05:00
Syed Sajjad Hussain Shah
c663f6fa30 Rebase 2u main with master (#1228)
* chore(deps): update dependency babel-plugin-formatjs to v10.5.14

* fix(deps): update dependency @edx/frontend-platform to v7.1.3

* fix(deps): update font awesome to v6.5.2

* chore(deps): update dependency @openedx/frontend-build to v13.1.4

* fix(deps): update dependency @openedx/paragon to v22.2.1

* fix(deps): update dependency algoliasearch to v4.23.3

* fix(deps): update dependency algoliasearch-helper to v3.17.0

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-17 17:00:28 +05:00
renovate[bot]
dba93333fd fix(deps): update dependency algoliasearch-helper to v3.17.0 2024-04-17 16:50:35 +05:00
renovate[bot]
611af07326 fix(deps): update dependency algoliasearch to v4.23.3 2024-04-17 16:50:35 +05:00
renovate[bot]
564ec70d9e fix(deps): update dependency @openedx/paragon to v22.2.1 2024-04-17 16:50:35 +05:00
renovate[bot]
65e95a4d1b chore(deps): update dependency @openedx/frontend-build to v13.1.4 2024-04-17 16:50:35 +05:00
renovate[bot]
cf2b50005b fix(deps): update font awesome to v6.5.2 2024-04-17 16:50:35 +05:00
renovate[bot]
faf4ff8488 fix(deps): update dependency @edx/frontend-platform to v7.1.3 2024-04-17 16:50:35 +05:00
renovate[bot]
7d64220852 chore(deps): update dependency babel-plugin-formatjs to v10.5.14 2024-04-17 16:50:35 +05:00
46 changed files with 864 additions and 203 deletions

View File

@@ -60,7 +60,7 @@ const InstitutionLogistration = props => {
className="btn nav-item p-0 mb-1 institutions--provider-link"
destination={lmsBaseUrl + provider.loginUrl}
>
{provider.name}
{provider?.name}
</Hyperlink>
</td>
</tr>

View File

@@ -5,7 +5,7 @@ import { Navigate } from 'react-router-dom';
import {
AUTHN_PROGRESSIVE_PROFILING, RECOMMENDATIONS, REDIRECT,
} from '../data/constants';
import { setCookie } from '../data/utils';
import setCookie from '../data/utils/cookies';
const RedirectLogistration = (props) => {
const {

View File

@@ -1,4 +1,5 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
@@ -8,15 +9,20 @@ import { Login } from '@openedx/paragon/icons';
import PropTypes from 'prop-types';
import messages from './messages';
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
import { LOGIN_PAGE, REGISTER_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
import { setCookie } from '../data/utils';
const SocialAuthProviders = (props) => {
const { formatMessage } = useIntl();
const { referrer, socialAuthProviders } = props;
const registrationFields = useSelector(state => state.register.registrationFormData);
function handleSubmit(e) {
e.preventDefault();
if (referrer === REGISTER_PAGE) {
setCookie('marketingEmailsOptIn', registrationFields?.configurableFormFields?.marketingEmailsOptIn);
}
const url = e.currentTarget.dataset.providerUrl;
window.location.href = getConfig().LMS_BASE_URL + url;
}

View File

@@ -0,0 +1,79 @@
export const registerFields = {
fields: {
country: {
name: 'country',
error_message: 'Select your country or region of residence',
},
honor_code: {
name: 'honor_code',
type: 'tos_and_honor_code',
error_message: '',
},
},
};
export const progressiveProfilingFields = {
extended_profile: [],
fields: {
level_of_education: {
name: 'level_of_education',
type: 'select',
label: 'Highest level of education completed',
error_message: '',
options: [
[
'p',
'Doctorate',
],
[
'm',
"Master's or professional degree",
],
[
'b',
"Bachelor's degree",
],
[
'a',
'Associate degree',
],
[
'hs',
'Secondary/high school',
],
[
'jhs',
'Junior secondary/junior high/middle school',
],
[
'none',
'No formal education',
],
[
'other',
'Other education',
],
],
},
gender: {
name: 'gender',
type: 'select',
label: 'Gender',
error_message: '',
options: [
[
'm',
'Male',
],
[
'f',
'Female',
],
[
'o',
'Other/Prefer Not to Say',
],
],
},
},
};

View File

@@ -1,3 +1,4 @@
import { getConfig } from '@edx/frontend-platform';
import { logError } from '@edx/frontend-platform/logging';
import { call, put, takeEvery } from 'redux-saga/effects';
@@ -7,6 +8,7 @@ import {
getThirdPartyAuthContextSuccess,
THIRD_PARTY_AUTH_CONTEXT,
} from './actions';
import { progressiveProfilingFields, registerFields } from './constants';
import {
getThirdPartyAuthContext,
} from './service';
@@ -20,7 +22,16 @@ export function* fetchThirdPartyAuthContext(action) {
} = yield call(getThirdPartyAuthContext, action.payload.urlParams);
yield put(setCountryFromThirdPartyAuthContext(thirdPartyAuthContext.countryCode));
yield put(getThirdPartyAuthContextSuccess(fieldDescriptions, optionalFields, thirdPartyAuthContext));
// hard code country field, level of education and gender fields
if (getConfig().ENABLE_HARD_CODE_OPTIONAL_FIELDS) {
yield put(getThirdPartyAuthContextSuccess(
registerFields,
progressiveProfilingFields,
thirdPartyAuthContext,
));
} else {
yield put(getThirdPartyAuthContextSuccess(fieldDescriptions, optionalFields, thirdPartyAuthContext));
}
} catch (e) {
yield put(getThirdPartyAuthContextFailure());
logError(e);

View File

@@ -1,16 +1,35 @@
import React from 'react';
import { Provider } from 'react-redux';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import renderer from 'react-test-renderer';
import configureStore from 'redux-mock-store';
import registerIcons from '../RegisterFaIcons';
import SocialAuthProviders from '../SocialAuthProviders';
registerIcons();
const mockStore = configureStore();
describe('SocialAuthProviders', () => {
let props = {};
const initialState = {
register: {
registrationFormData: {
configurableFormFields: {
marketingEmailsOptIn: true,
},
},
},
};
const store = mockStore(initialState);
const reduxWrapper = children => (
<IntlProvider locale="en">
<Provider store={store}>{children}</Provider>
</IntlProvider>
);
const appleProvider = {
id: 'oa2-apple-id',
name: 'Apple',
@@ -30,11 +49,11 @@ describe('SocialAuthProviders', () => {
it('should match social auth provider with iconImage snapshot', () => {
props = { socialAuthProviders: [appleProvider, facebookProvider] };
const tree = renderer.create(
const tree = renderer.create(reduxWrapper(
<IntlProvider locale="en">
<SocialAuthProviders {...props} />
</IntlProvider>,
).toJSON();
)).toJSON();
expect(tree).toMatchSnapshot();
});
@@ -48,11 +67,11 @@ describe('SocialAuthProviders', () => {
}],
};
const tree = renderer.create(
const tree = renderer.create(reduxWrapper(
<IntlProvider locale="en">
<SocialAuthProviders {...props} />
</IntlProvider>,
).toJSON();
)).toJSON();
expect(tree).toMatchSnapshot();
});
@@ -66,11 +85,11 @@ describe('SocialAuthProviders', () => {
}],
};
const tree = renderer.create(
const tree = renderer.create(reduxWrapper(
<IntlProvider locale="en">
<SocialAuthProviders {...props} />
</IntlProvider>,
).toJSON();
)).toJSON();
expect(tree).toMatchSnapshot();
});

View File

@@ -11,6 +11,7 @@ const configuration = {
MARKETING_EMAILS_OPT_IN: process.env.MARKETING_EMAILS_OPT_IN || '',
SHOW_CONFIGURABLE_EDX_FIELDS: process.env.SHOW_CONFIGURABLE_EDX_FIELDS || false,
SHOW_REGISTRATION_LINKS: process.env.SHOW_REGISTRATION_LINKS !== 'false',
ENABLE_HARD_CODE_OPTIONAL_FIELDS: process.env.ENABLE_HARD_CODE_OPTIONAL_FIELDS || false,
ENABLE_IMAGE_LAYOUT: process.env.ENABLE_IMAGE_LAYOUT || false,
// Links
ACTIVATION_EMAIL_SUPPORT_LINK: process.env.ACTIVATION_EMAIL_SUPPORT_LINK || null,
@@ -35,6 +36,7 @@ const configuration = {
ZENDESK_LOGO_URL: process.env.ZENDESK_LOGO_URL,
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 || '',
};
export default configuration;

View File

@@ -35,5 +35,6 @@ export const VALID_EMAIL_REGEX = '(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+
// Query string parameters that can be passed to LMS to manage
// things like auto-enrollment upon login and registration.
export const AUTH_PARAMS = ['course_id', 'enrollment_action', 'course_mode', 'email_opt_in', 'purchase_workflow', 'next', 'register_for_free', 'track', 'is_account_recovery', 'variant', 'host', 'cta'];
export const AUTH_PARAMS = ['course_id', 'enrollment_action', 'course_mode', 'country', 'email_opt_in', 'purchase_workflow', 'next', 'register_for_free', 'track', 'is_account_recovery', 'variant', 'host', 'cta', 'levelOfEducation', 'finishAuthUrl'];
export const REDIRECT = 'redirect';
export const APP_NAME = 'authn_mfe';

37
src/data/segment/utils.js Normal file
View File

@@ -0,0 +1,37 @@
/* eslint-disable import/prefer-default-export */
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { APP_NAME } from '../constants';
export const LINK_TIMEOUT = 300;
/**
* Creates an event tracker function that sends a tracking event with the given name and options.
*
* @param {string} name - The name of the event to be tracked.
* @param {object} [options={}] - Additional options to be included with the event.
* @returns {function} - A function that, when called, sends the tracking event.
*/
export const createEventTracker = (name, options = {}) => () => sendTrackEvent(
name,
{ ...options, app_name: APP_NAME },
);
/**
* Creates an event tracker function that sends a tracking event with the given name and options.
*
* @param {string} name - The name of the event to be tracked.
* @param {object} [options={}] - Additional options to be included with the event.
* @returns {function} - A function that, when called, sends the tracking event.
*/
export const createPageEventTracker = (name, options = null) => () => sendPageEvent(
name,
options,
{ app_name: APP_NAME },
);
export const createLinkTracker = (tracker, href) => (e) => {
e.preventDefault();
tracker();
return setTimeout(() => { window.location.href = href; }, LINK_TIMEOUT);
};

View File

@@ -11,3 +11,11 @@ export default function setCookie(cookieName, cookieValue, cookieExpiry) {
cookies.set(cookieName, cookieValue, options);
}
}
export function removeCookie(cookieName) {
if (cookieName) {
const cookies = new Cookies();
const options = { domain: getConfig().SESSION_COOKIE_DOMAIN, path: '/' };
cookies.remove(cookieName, options);
}
}

View File

@@ -8,4 +8,4 @@ export {
windowScrollTo,
} from './dataUtils';
export { default as AsyncActionType } from './reduxUtils';
export { default as setCookie } from './cookies';
export { default as setCookie, removeCookie } from './cookies';

View File

@@ -2,7 +2,6 @@ import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Form,
@@ -25,6 +24,10 @@ import BaseContainer from '../base-container';
import { FormGroup } from '../common-components';
import { DEFAULT_STATE, LOGIN_PAGE, VALID_EMAIL_REGEX } from '../data/constants';
import { updatePathWithQueryParams, windowScrollTo } from '../data/utils';
import {
trackForgotPasswordPageEvent,
trackForgotPasswordPageViewed,
} from '../tracking/trackers/forgotpassword';
const ForgotPasswordPage = (props) => {
const platformName = getConfig().SITE_NAME;
@@ -41,8 +44,8 @@ const ForgotPasswordPage = (props) => {
const navigate = useNavigate();
useEffect(() => {
sendPageEvent('login_and_registration', 'reset');
sendTrackEvent('edx.bi.password_reset_form.viewed', { category: 'user-engagement' });
trackForgotPasswordPageEvent();
trackForgotPasswordPageViewed();
}, []);
useEffect(() => {

View File

@@ -2,7 +2,6 @@ import React, { useEffect, useMemo, useState } from 'react';
import { connect } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { injectIntl, useIntl } from '@edx/frontend-platform/i18n';
import {
Form, StatefulButton,
@@ -43,6 +42,9 @@ import {
updatePathWithQueryParams,
} from '../data/utils';
import ResetPasswordSuccess from '../reset-password/ResetPasswordSuccess';
import {
trackForgotPasswordLinkClick, trackLoginPageViewed, trackLoginSuccess,
} from '../tracking/trackers/login';
const LoginPage = (props) => {
const {
@@ -78,9 +80,15 @@ const LoginPage = (props) => {
const tpaHint = getTpaHint();
useEffect(() => {
sendPageEvent('login_and_registration', 'login');
trackLoginPageViewed();
}, []);
useEffect(() => {
if (loginResult.success) {
trackLoginSuccess();
}
}, [loginResult]);
useEffect(() => {
const payload = { ...queryParams };
if (tpaHint) {
@@ -170,9 +178,6 @@ const LoginPage = (props) => {
const { name } = event.target;
setErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
};
const trackForgotPasswordLinkClick = () => {
sendTrackEvent('edx.bi.password-reset_form.toggled', { category: 'user-engagement' });
};
const { provider, skipHintedLogin } = getTpaProvider(tpaHint, providers, secondaryProviders);

View File

@@ -11,7 +11,9 @@ import { act } from 'react-dom/test-utils';
import { MemoryRouter } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import { COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE } from '../../data/constants';
import {
APP_NAME, COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE,
} from '../../data/constants';
import { backupLoginFormBegin, dismissPasswordResetBanner, loginRequest } from '../data/actions';
import { INTERNAL_SERVER_ERROR } from '../data/constants';
import LoginPage from '../LoginPage';
@@ -751,7 +753,7 @@ describe('LoginPage', () => {
it('should send page event when login page is rendered', () => {
render(reduxWrapper(<IntlLoginPage {...props} />));
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'login');
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'login', { app_name: APP_NAME });
});
it('tests that form is in invalid state when it is submitted', () => {
@@ -784,7 +786,7 @@ describe('LoginPage', () => {
{ selector: '#forgot-password' },
));
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.password-reset_form.toggled', { category: 'user-engagement' });
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.password-reset_form.toggled', { category: 'user-engagement', app_name: APP_NAME });
});
it('should backup the login form state when shouldBackupState is true', () => {

View File

@@ -20,7 +20,7 @@ import {
tpaProvidersSelector,
} from '../common-components/data/selectors';
import messages from '../common-components/messages';
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
import { APP_NAME, LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
import {
getTpaHint, getTpaProvider, updatePathWithQueryParams,
} from '../data/utils';
@@ -56,11 +56,11 @@ const Logistration = (props) => {
}, [navigate, disablePublicAccountCreation]);
const handleInstitutionLogin = (e) => {
sendTrackEvent('edx.bi.institution_login_form.toggled', { category: 'user-engagement' });
sendTrackEvent('edx.bi.institution_login_form.toggled', { category: 'user-engagement', app_name: APP_NAME });
if (typeof e === 'string') {
sendPageEvent('login_and_registration', e === '/login' ? 'login' : 'register');
sendPageEvent('login_and_registration', e === '/login' ? 'login' : 'register', { app_name: APP_NAME });
} else {
sendPageEvent('login_and_registration', e.target.dataset.eventName);
sendPageEvent('login_and_registration', e.target.dataset.eventName, { app_name: APP_NAME });
}
setInstitutionLogin(!institutionLogin);
@@ -70,7 +70,7 @@ const Logistration = (props) => {
if (tabKey === currentTab) {
return;
}
sendTrackEvent(`edx.bi.${tabKey.replace('/', '')}_form.toggled`, { category: 'user-engagement' });
sendTrackEvent(`edx.bi.${tabKey.replace('/', '')}_form.toggled`, { category: 'user-engagement', app_name: APP_NAME });
props.clearThirdPartyAuthContextErrorMessage();
if (tabKey === LOGIN_PAGE) {
props.backupRegistrationForm();

View File

@@ -11,16 +11,21 @@ import configureStore from 'redux-mock-store';
import Logistration from './Logistration';
import { clearThirdPartyAuthContextErrorMessage } from '../common-components/data/actions';
import {
APP_NAME,
COMPLETE_STATE, LOGIN_PAGE, REGISTER_PAGE,
} from '../data/constants';
import { backupLoginForm } from '../login/data/actions';
import { backupRegistrationForm } from '../register/data/actions';
import { NOT_INITIALIZED } from '../register/data/optimizelyExperiment/helper';
import useAutoGeneratedUsernameExperimentVariation
from '../register/data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
jest.mock('@edx/frontend-platform/analytics', () => ({
sendPageEvent: jest.fn(),
sendTrackEvent: jest.fn(),
}));
jest.mock('@edx/frontend-platform/auth');
jest.mock('../register/data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
const mockStore = configureStore();
const IntlLogistration = injectIntl(Logistration);
@@ -84,6 +89,7 @@ describe('Logistration', () => {
})),
}));
useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
configure({
loggingService: { logError: jest.fn() },
config: {
@@ -224,8 +230,8 @@ describe('Logistration', () => {
render(reduxWrapper(<IntlLogistration {...props} />));
fireEvent.click(screen.getByText('Institution/campus credentials'));
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.institution_login_form.toggled', { category: 'user-engagement' });
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'institution_login');
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.institution_login_form.toggled', { category: 'user-engagement', app_name: APP_NAME });
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'institution_login', { app_name: APP_NAME });
mergeConfig({
DISABLE_ENTERPRISE_LOGIN: '',

View File

@@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { getConfig, snakeCaseObject } from '@edx/frontend-platform';
import { identifyAuthenticatedUser, sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { identifyAuthenticatedUser } from '@edx/frontend-platform/analytics';
import {
AxiosJwtAuthService,
configure as configureAuth,
@@ -39,6 +39,13 @@ import {
import isOneTrustFunctionalCookieEnabled from '../data/oneTrust';
import { getAllPossibleQueryParams, isHostAvailableInQueryParams } from '../data/utils';
import { FormFieldRenderer } from '../field-renderer';
import {
trackDisablePostRegistrationRecommendations,
trackProgressiveProfilingPageViewed,
trackProgressiveProfilingSkipLinkClick,
trackProgressiveProfilingSubmitClick,
trackProgressiveProfilingSupportLinkCLick,
} from '../tracking/trackers/progressive-profiling';
const ProgressiveProfiling = (props) => {
const { formatMessage } = useIntl();
@@ -98,14 +105,13 @@ const ProgressiveProfiling = (props) => {
useEffect(() => {
if (authenticatedUser?.userId) {
identifyAuthenticatedUser(authenticatedUser.userId);
sendPageEvent('login_and_registration', 'welcome');
trackProgressiveProfilingPageViewed();
}
}, [authenticatedUser]);
useEffect(() => {
if (!enablePostRegistrationRecommendations) {
sendTrackEvent(
'edx.bi.user.recommendations.not.enabled',
trackDisablePostRegistrationRecommendations(
{ functionalCookiesConsent, page: 'authn_recommendations' },
);
return;
@@ -149,29 +155,23 @@ const ProgressiveProfiling = (props) => {
});
}
props.saveUserProfile(authenticatedUser.username, snakeCaseObject(payload));
sendTrackEvent(
'edx.bi.welcome.page.submit.clicked',
{
isGenderSelected: !!values.gender,
isYearOfBirthSelected: !!values.year_of_birth,
isLevelOfEducationSelected: !!values.level_of_education,
isWorkExperienceSelected: !!values.work_experience,
host: queryParams?.host || '',
},
);
const eventProperties = {
isGenderSelected: !!values.gender,
isYearOfBirthSelected: !!values.year_of_birth,
isLevelOfEducationSelected: !!values.level_of_education,
isWorkExperienceSelected: !!values.work_experience,
host: queryParams?.host || '',
};
trackProgressiveProfilingSubmitClick(eventProperties);
};
const handleSkip = (e) => {
e.preventDefault();
window.history.replaceState(location.state, null, '');
setShowModal(true);
sendTrackEvent(
'edx.bi.welcome.page.skip.link.clicked',
{
host: queryParams?.host || '',
},
);
trackProgressiveProfilingSkipLinkClick({
host: queryParams?.host || '',
});
};
const onChangeHandler = (e) => {
@@ -242,7 +242,7 @@ const ProgressiveProfiling = (props) => {
destination={getConfig().AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK}
target="_blank"
showLaunchIcon={false}
onClick={() => (sendTrackEvent('edx.bi.welcome.page.support.link.clicked'))}
onClick={() => (trackProgressiveProfilingSupportLinkCLick())}
>
{formatMessage(messages['optional.fields.information.link'])}
</Hyperlink>

View File

@@ -12,6 +12,7 @@ import { MemoryRouter, mockNavigate, useLocation } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import {
APP_NAME,
AUTHN_PROGRESSIVE_PROFILING,
COMPLETE_STATE, DEFAULT_REDIRECT_URL,
EMBEDDED,
@@ -143,8 +144,9 @@ describe('ProgressiveProfilingTests', () => {
const modalContentContainer = document.getElementsByClassName('.pgn__modal-content-container');
expect(modalContentContainer).toBeTruthy();
const payload = { host: '', app_name: APP_NAME };
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.skip.link.clicked', { host: '' });
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.skip.link.clicked', payload);
});
// ******** test event functionality ********
@@ -165,7 +167,7 @@ describe('ProgressiveProfilingTests', () => {
const supportLink = screen.getByRole('link', { name: /learn more about how we use this information/i });
fireEvent.click(supportLink);
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.support.link.clicked');
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.support.link.clicked', { app_name: APP_NAME });
});
it('should set empty host property value for non-embedded experience', () => {
@@ -175,6 +177,7 @@ describe('ProgressiveProfilingTests', () => {
isLevelOfEducationSelected: false,
isWorkExperienceSelected: false,
host: '',
app_name: APP_NAME,
};
delete window.location;
window.location = { href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING) };
@@ -316,7 +319,7 @@ describe('ProgressiveProfilingTests', () => {
const skipLinkButton = screen.getByText('Skip for now');
fireEvent.click(skipLinkButton);
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.skip.link.clicked', { host });
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.skip.link.clicked', { host, app_name: APP_NAME });
});
it('should show spinner while fetching the optional fields', () => {
@@ -349,6 +352,7 @@ describe('ProgressiveProfilingTests', () => {
isLevelOfEducationSelected: false,
isWorkExperienceSelected: false,
host: 'http://example.com',
app_name: APP_NAME,
};
delete window.location;
window.location = {

View File

@@ -1,7 +1,8 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
breakpoints,
@@ -21,18 +22,35 @@ import RecommendationsLargeLayout from './RecommendationsPageLayouts/LargeLayout
import RecommendationsSmallLayout from './RecommendationsPageLayouts/SmallLayout';
import { LINK_TIMEOUT, trackRecommendationsViewed, trackSkipButtonClicked } from './track';
import { DEFAULT_REDIRECT_URL } from '../data/constants';
import { getAllPossibleQueryParams } from '../data/utils';
const RecommendationsPage = () => {
const { formatMessage } = useIntl();
const DASHBOARD_URL = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
const isExtraSmall = useMediaQuery({ maxWidth: breakpoints.extraSmall.maxWidth - 1 });
const location = useLocation();
const queryParams = getAllPossibleQueryParams();
// flag to show recommendations for onboarding component experience
const showRecommendations = !!queryParams?.finalRedirectUrl && !!queryParams?.country;
const backendCountryCode = useSelector((state) => state.register.backendCountryCode);
const registrationResponse = location.state?.registrationResult;
const DASHBOARD_URL = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
const educationLevel = EDUCATION_LEVEL_MAPPING[location.state?.educationLevel];
const userId = location.state?.userId;
const [redirectUrl, setRedirectUrl] = useState(location.state?.registrationResult?.redirectUrl);
const [educationLevel, setEducationLevel] = useState(EDUCATION_LEVEL_MAPPING[location.state?.educationLevel]);
const [userId, setUserId] = useState(location.state?.userId || null);
const [userCountry, setUserCountry] = useState(backendCountryCode);
useEffect(() => {
if (showRecommendations) {
const authenticatedUser = getAuthenticatedUser();
if (authenticatedUser) {
setRedirectUrl(queryParams.finalRedirectUrl);
setEducationLevel(EDUCATION_LEVEL_MAPPING[queryParams?.levelOfEducation]);
setUserCountry(queryParams.country);
setUserId(authenticatedUser?.userId);
}
}
}, [showRecommendations, queryParams]);
const userCountry = useSelector((state) => state.register.backendCountryCode);
const {
recommendations: algoliaRecommendations,
isLoading,
@@ -46,8 +64,8 @@ const RecommendationsPage = () => {
const handleSkipRecommendationPage = () => {
window.history.replaceState(location.state, null, '');
if (registrationResponse) {
window.location.href = registrationResponse.redirectUrl;
if (redirectUrl) {
window.location.href = redirectUrl;
} else {
window.location.href = DASHBOARD_URL;
}
@@ -59,7 +77,7 @@ const RecommendationsPage = () => {
setTimeout(() => { handleSkipRecommendationPage(); }, LINK_TIMEOUT);
};
if (!registrationResponse) {
if (!redirectUrl && !showRecommendations) {
window.location.href = DASHBOARD_URL;
return null;
}

View File

@@ -101,7 +101,7 @@ const UsernameField = (props) => {
};
const suggestedUsernames = () => (
<div className={className} role="listbox">
<div className={className}>
<span className="text-gray username-suggestion--label">{formatMessage(messages['registration.username.suggestion.label'])}</span>
<div className="username-scroll-suggested--form-field">
{usernameSuggestions.map((username, index) => (
@@ -112,9 +112,7 @@ const UsernameField = (props) => {
className="username-suggestions--chip data-hj-suppress"
autoComplete={props.autoComplete}
key={`suggestion-${index.toString()}`}
tabIndex={0}
onClick={(e) => handleSuggestionClick(e, username)}
role="option"
>
{username}
</Button>
@@ -125,7 +123,7 @@ const UsernameField = (props) => {
);
if (usernameSuggestions.length > 0 && errorMessage && value === ' ') {
className = 'username-suggestions';
className = 'username-suggestions__error';
iconButton = <IconButton src={Close} iconAs={Icon} alt="Close" onClick={() => handleUsernameSuggestionClose()} variant="black" size="sm" className="username-suggestions__close__button" />;
suggestedUsernameDiv = suggestedUsernames();
} else if (usernameSuggestions.length > 0 && value === ' ') {
@@ -136,15 +134,14 @@ const UsernameField = (props) => {
suggestedUsernameDiv = suggestedUsernames();
}
return (
<div className="username__form-group-wrapper">
<FormGroup
{...props}
handleChange={handleOnChange}
handleFocus={handleOnFocus}
handleBlur={handleOnBlur}
>
{suggestedUsernameDiv}
<FormGroup
{...props}
handleChange={handleOnChange}
handleFocus={handleOnFocus}
handleBlur={handleOnBlur}
/>
</div>
</FormGroup>
);
};

View File

@@ -4,7 +4,6 @@ import React, {
import { useDispatch, useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Form, Spinner, StatefulButton } from '@openedx/paragon';
import classNames from 'classnames';
@@ -18,6 +17,7 @@ import {
backupRegistrationFormBegin,
clearRegistrationBackendError,
registerNewUser,
setAutoGeneratedUsernameExperimentData,
setEmailSuggestionInStore,
setUserPipelineDataLoaded,
} from './data/actions';
@@ -25,6 +25,8 @@ import {
FORM_SUBMISSION_ERROR,
TPA_AUTHENTICATION_FAILURE,
} from './data/constants';
import { AUTO_GENERATED_USERNAME_REGISTRATION_EXP_VARIATION, NOT_INITIALIZED } from './data/optimizelyExperiment/helper';
import useAutoGeneratedUsernameExperimentVariation from './data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
import getBackendValidations from './data/selectors';
import {
isFormValid, prepareRegistrationPayload,
@@ -41,11 +43,12 @@ import { getThirdPartyAuthContext as getRegistrationDataFromBackend } from '../c
import EnterpriseSSO from '../common-components/EnterpriseSSO';
import ThirdPartyAuth from '../common-components/ThirdPartyAuth';
import {
COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE,
APP_NAME, COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE,
} from '../data/constants';
import {
getAllPossibleQueryParams, getTpaHint, getTpaProvider, isHostAvailableInQueryParams, setCookie,
getAllPossibleQueryParams, getTpaHint, getTpaProvider, isHostAvailableInQueryParams, removeCookie, setCookie,
} from '../data/utils';
import { trackRegistrationPageViewed, trackRegistrationSuccess } from '../tracking/trackers/register';
/**
* Main Registration Page component
@@ -68,6 +71,7 @@ const RegistrationPage = (props) => {
} = props;
const backedUpFormData = useSelector(state => state.register.registrationFormData);
const initExpVariation = useSelector(state => state.register.autoGeneratedUsernameExperimentVariation);
const registrationError = useSelector(state => state.register.registrationError);
const registrationErrorCode = registrationError?.errorCode;
const registrationResult = useSelector(state => state.register.registrationResult);
@@ -103,6 +107,12 @@ const RegistrationPage = (props) => {
? formatMessage(messages['create.account.cta.button'], { label: cta })
: formatMessage(messages['create.account.for.free.button']);
const autoGeneratedUsernameExpVariation = useAutoGeneratedUsernameExperimentVariation(
initExpVariation, registrationEmbedded, tpaHint, currentProvider, thirdPartyAuthApiStatus,
);
const hideUsernameField = flags.autoGeneratedUsernameEnabled
|| autoGeneratedUsernameExpVariation === AUTO_GENERATED_USERNAME_REGISTRATION_EXP_VARIATION;
/**
* Set the userPipelineDetails data in formFields for only first time
*/
@@ -128,7 +138,7 @@ const RegistrationPage = (props) => {
useEffect(() => {
if (!formStartTime) {
sendPageEvent('login_and_registration', 'register');
trackRegistrationPageViewed();
const payload = { ...queryParams, is_register_page: true };
if (tpaHint) {
payload.tpa_hint = tpaHint;
@@ -149,8 +159,10 @@ const RegistrationPage = (props) => {
formFields: { ...formFields },
errors: { ...errors },
}));
dispatch(setAutoGeneratedUsernameExperimentData(autoGeneratedUsernameExpVariation));
}
}, [shouldBackupState, configurableFormFields, formFields, errors, dispatch, backedUpFormData]);
}, [shouldBackupState, configurableFormFields, // eslint-disable-line react-hooks/exhaustive-deps
formFields, errors, dispatch, backedUpFormData]);
useEffect(() => {
if (backendValidations) {
@@ -171,10 +183,13 @@ const RegistrationPage = (props) => {
useEffect(() => {
if (registrationResult.success) {
// This event is used by GTM
sendTrackEvent('edx.bi.user.account.registered.client', {});
trackRegistrationSuccess();
// This is used by the "User Retention Rate Event" on GTM
setCookie(getConfig().USER_RETENTION_COOKIE_NAME, true);
// remove marketingEmailsOptIn cookie that was set on SSO registration flow
removeCookie('marketingEmailsOptIn');
}
}, [registrationResult]);
@@ -210,13 +225,13 @@ const RegistrationPage = (props) => {
const registerUser = () => {
const totalRegistrationTime = (Date.now() - formStartTime) / 1000;
let payload = { ...formFields };
let payload = { ...formFields, app_name: APP_NAME };
if (currentProvider) {
delete payload.password;
payload.social_auth_provider = currentProvider;
}
if (flags.autoGeneratedUsernameEnabled) {
if (hideUsernameField) {
delete payload.username;
}
@@ -286,106 +301,109 @@ const RegistrationPage = (props) => {
getConfig().ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN && !!Object.keys(optionalFields.fields).length
}
/>
{autoSubmitRegForm && !errorCode.type ? (
<div className="mw-xs mt-5 text-center">
<Spinner animation="border" variant="primary" id="tpa-spinner" />
</div>
) : (
<div
className={classNames(
'mw-xs mt-3',
{ 'w-100 m-auto pt-4 main-content': registrationEmbedded },
)}
>
<ThirdPartyAuthAlert
currentProvider={currentProvider}
platformName={platformName}
referrer={REGISTER_PAGE}
/>
<RegistrationFailure
errorCode={errorCode.type}
failureCount={errorCode.count}
context={{ provider: currentProvider, errorMessage: thirdPartyAuthErrorMessage }}
/>
<Form id="registration-form" name="registration-form">
<NameField
name="name"
value={formFields.name}
shouldFetchUsernameSuggestions={!formFields.username.trim()}
handleChange={handleOnChange}
handleErrorChange={handleErrorChange}
errorMessage={errors.name}
helpText={[formatMessage(messages['help.text.name'])]}
floatingLabel={formatMessage(messages['registration.fullname.label'])}
{(autoSubmitRegForm && !errorCode.type)
|| (!autoGeneratedUsernameExpVariation && !(
autoGeneratedUsernameExpVariation === NOT_INITIALIZED
|| registrationEmbedded || !!tpaHint || !!currentProvider))
? (
<div className="mw-xs mt-5 text-center">
<Spinner animation="border" variant="primary" id="tpa-spinner" />
</div>
) : (
<div
className={classNames(
'mw-xs mt-3',
{ 'w-100 m-auto pt-4 main-content': registrationEmbedded },
)}
>
<ThirdPartyAuthAlert
currentProvider={currentProvider}
platformName={platformName}
referrer={REGISTER_PAGE}
/>
<EmailField
name="email"
value={formFields.email}
confirmEmailValue={configurableFormFields?.confirm_email}
handleErrorChange={handleErrorChange}
handleChange={handleOnChange}
errorMessage={errors.email}
helpText={[formatMessage(messages['help.text.email'])]}
floatingLabel={formatMessage(messages['registration.email.label'])}
<RegistrationFailure
errorCode={errorCode.type}
failureCount={errorCode.count}
context={{ provider: currentProvider, errorMessage: thirdPartyAuthErrorMessage }}
/>
{!flags.autoGeneratedUsernameEnabled && (
<UsernameField
name="username"
spellCheck="false"
value={formFields.username}
<Form id="registration-form" name="registration-form">
<NameField
name="name"
value={formFields.name}
shouldFetchUsernameSuggestions={!formFields.username.trim()}
handleChange={handleOnChange}
handleErrorChange={handleErrorChange}
errorMessage={errors.username}
helpText={[formatMessage(messages['help.text.username.1']), formatMessage(messages['help.text.username.2'])]}
floatingLabel={formatMessage(messages['registration.username.label'])}
errorMessage={errors.name}
helpText={[formatMessage(messages['help.text.name'])]}
floatingLabel={formatMessage(messages['registration.fullname.label'])}
/>
)}
{!currentProvider && (
<PasswordField
name="password"
value={formFields.password}
handleChange={handleOnChange}
<EmailField
name="email"
value={formFields.email}
confirmEmailValue={configurableFormFields?.confirm_email}
handleErrorChange={handleErrorChange}
errorMessage={errors.password}
floatingLabel={formatMessage(messages['registration.password.label'])}
handleChange={handleOnChange}
errorMessage={errors.email}
helpText={[formatMessage(messages['help.text.email'])]}
floatingLabel={formatMessage(messages['registration.email.label'])}
/>
)}
<ConfigurableRegistrationForm
email={formFields.email}
fieldErrors={errors}
formFields={configurableFormFields}
setFieldErrors={registrationEmbedded ? setTemporaryErrors : setErrors}
setFormFields={setConfigurableFormFields}
autoSubmitRegisterForm={autoSubmitRegForm}
fieldDescriptions={fieldDescriptions}
/>
<StatefulButton
id="register-user"
name="register-user"
type="submit"
variant="brand"
className="register-button mt-4 mb-4"
state={submitState}
labels={{
default: buttonLabel,
pending: '',
}}
onClick={handleSubmit}
onMouseDown={(e) => e.preventDefault()}
/>
{!registrationEmbedded && (
<ThirdPartyAuth
currentProvider={currentProvider}
providers={providers}
secondaryProviders={secondaryProviders}
handleInstitutionLogin={handleInstitutionLogin}
thirdPartyAuthApiStatus={thirdPartyAuthApiStatus}
{!hideUsernameField && (
<UsernameField
name="username"
spellCheck="false"
value={formFields.username}
handleChange={handleOnChange}
handleErrorChange={handleErrorChange}
errorMessage={errors.username}
helpText={[formatMessage(messages['help.text.username.1']), formatMessage(messages['help.text.username.2'])]}
floatingLabel={formatMessage(messages['registration.username.label'])}
/>
)}
{!currentProvider && (
<PasswordField
name="password"
value={formFields.password}
handleChange={handleOnChange}
handleErrorChange={handleErrorChange}
errorMessage={errors.password}
floatingLabel={formatMessage(messages['registration.password.label'])}
/>
)}
<ConfigurableRegistrationForm
email={formFields.email}
fieldErrors={errors}
formFields={configurableFormFields}
setFieldErrors={registrationEmbedded ? setTemporaryErrors : setErrors}
setFormFields={setConfigurableFormFields}
autoSubmitRegisterForm={autoSubmitRegForm}
fieldDescriptions={fieldDescriptions}
/>
)}
</Form>
</div>
)}
<StatefulButton
id="register-user"
name="register-user"
type="submit"
variant="brand"
className="register-button mt-4 mb-4"
state={submitState}
labels={{
default: buttonLabel,
pending: '',
}}
onClick={handleSubmit}
onMouseDown={(e) => e.preventDefault()}
/>
{!registrationEmbedded && (
<ThirdPartyAuth
currentProvider={currentProvider}
providers={providers}
secondaryProviders={secondaryProviders}
handleInstitutionLogin={handleInstitutionLogin}
thirdPartyAuthApiStatus={thirdPartyAuthApiStatus}
/>
)}
</Form>
</div>
)}
</>
);
};

View File

@@ -17,9 +17,12 @@ import {
setUserPipelineDataLoaded,
} from './data/actions';
import { INTERNAL_SERVER_ERROR } from './data/constants';
import { NOT_INITIALIZED } from './data/optimizelyExperiment/helper';
import useAutoGeneratedUsernameExperimentVariation
from './data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
import RegistrationPage from './RegistrationPage';
import {
AUTHN_PROGRESSIVE_PROFILING, COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE,
APP_NAME, AUTHN_PROGRESSIVE_PROFILING, COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE,
} from '../data/constants';
jest.mock('@edx/frontend-platform/analytics', () => ({
@@ -30,6 +33,7 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(),
}));
jest.mock('./data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
const IntlRegistrationPage = injectIntl(RegistrationPage);
const mockStore = configureStore();
@@ -128,6 +132,7 @@ describe('RegistrationPage', () => {
institutionLogin: false,
};
window.location = { search: '' };
useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
});
afterEach(() => {
@@ -185,6 +190,7 @@ describe('RegistrationPage', () => {
honor_code: true,
totalRegistrationTime: 0,
next: '/course/demo-course-url',
app_name: APP_NAME,
};
store.dispatch = jest.fn(store.dispatch);
@@ -207,6 +213,7 @@ describe('RegistrationPage', () => {
honor_code: true,
social_auth_provider: 'Apple',
totalRegistrationTime: 0,
app_name: APP_NAME,
};
store = mockStore({
@@ -292,6 +299,7 @@ describe('RegistrationPage', () => {
honor_code: true,
totalRegistrationTime: 0,
marketing_emails_opt_in: true,
app_name: APP_NAME,
};
store.dispatch = jest.fn(store.dispatch);
@@ -318,6 +326,7 @@ describe('RegistrationPage', () => {
country: 'Pakistan',
honor_code: true,
totalRegistrationTime: 0,
app_name: APP_NAME,
};
store.dispatch = jest.fn(store.dispatch);
@@ -592,7 +601,7 @@ describe('RegistrationPage', () => {
it('should send page event when register page is rendered', () => {
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'register');
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'register', { app_name: APP_NAME });
});
it('should send track event when user has successfully registered', () => {
@@ -610,7 +619,7 @@ describe('RegistrationPage', () => {
delete window.location;
window.location = { href: getConfig().BASE_URL };
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.user.account.registered.client', {});
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.user.account.registered.client', { app_name: APP_NAME });
});
it('should populate form with pipeline user details', () => {
@@ -883,6 +892,7 @@ describe('RegistrationPage', () => {
country: 'PK',
social_auth_provider: 'Apple',
totalRegistrationTime: 0,
app_name: APP_NAME,
}));
});
});

View File

@@ -1,10 +1,12 @@
import React, { useEffect, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { getCountryList, getLocale, useIntl } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types';
import { FormFieldRenderer } from '../../field-renderer';
import { backupRegistrationFormBegin } from '../data/actions';
import { FIELDS } from '../data/constants';
import messages from '../messages';
import { CountryField, HonorCode, TermsOfService } from '../RegistrationFields';
@@ -32,6 +34,7 @@ const ConfigurableRegistrationForm = (props) => {
setFormFields,
autoSubmitRegistrationForm,
} = props;
const dispatch = useDispatch();
/** The reason for adding the entry 'United States' is that Chrome browser aut-fill the form with the 'Unites
States' instead of 'United States of America' which does not exist in country dropdown list and gets the user
@@ -50,6 +53,8 @@ const ConfigurableRegistrationForm = (props) => {
showMarketingEmailOptInCheckbox: getConfig().MARKETING_EMAILS_OPT_IN,
};
const backedUpFormData = useSelector(state => state.register.registrationFormData);
/**
* If auto submitting register form, we will check tos and honor code fields if they exist for feature parity.
*/
@@ -90,6 +95,16 @@ const ConfigurableRegistrationForm = (props) => {
setFieldErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
}
}
// setting marketingEmailsOptIn state for SSO authentication flow for register API call
if (name === 'marketingEmailsOptIn') {
dispatch(backupRegistrationFormBegin({
...backedUpFormData,
configurableFormFields: {
...backedUpFormData.configurableFormFields,
[name]: value,
},
}));
}
setFormFields(prevState => ({ ...prevState, [name]: value }));
};

View File

@@ -9,8 +9,12 @@ import { fireEvent, render } from '@testing-library/react';
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';
import useAutoGeneratedUsernameExperimentVariation
from '../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
import RegistrationPage from '../../RegistrationPage';
import ConfigurableRegistrationForm from '../ConfigurableRegistrationForm';
@@ -22,6 +26,7 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(),
}));
jest.mock('../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
const IntlConfigurableRegistrationForm = injectIntl(ConfigurableRegistrationForm);
const IntlRegistrationPage = injectIntl(RegistrationPage);
@@ -121,6 +126,7 @@ describe('ConfigurableRegistrationForm', () => {
};
window.location = { search: '' };
getLocale.mockImplementationOnce(() => ('en-us'));
useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
});
afterEach(() => {
@@ -260,7 +266,7 @@ describe('ConfigurableRegistrationForm', () => {
fireEvent.click(submitButton);
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK', app_name: APP_NAME }));
});
it('should show error messages for required fields on empty form submission', () => {

View File

@@ -12,6 +12,9 @@ import configureStore from 'redux-mock-store';
import {
FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR, TPA_AUTHENTICATION_FAILURE, TPA_SESSION_EXPIRED,
} from '../../data/constants';
import { NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
import useAutoGeneratedUsernameExperimentVariation
from '../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
import RegistrationPage from '../../RegistrationPage';
import RegistrationFailureMessage from '../RegistrationFailure';
@@ -23,6 +26,7 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(),
}));
jest.mock('../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
const IntlRegistrationPage = injectIntl(RegistrationPage);
const IntlRegistrationFailure = injectIntl(RegistrationFailureMessage);
@@ -121,6 +125,7 @@ describe('RegistrationFailure', () => {
institutionLogin: false,
};
window.location = { search: '' };
useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
});
afterEach(() => {

View File

@@ -12,6 +12,9 @@ import configureStore from 'redux-mock-store';
import {
COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE, REGISTER_PAGE,
} from '../../../data/constants';
import { NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
import useAutoGeneratedUsernameExperimentVariation
from '../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
import RegistrationPage from '../../RegistrationPage';
jest.mock('@edx/frontend-platform/analytics', () => ({
@@ -22,6 +25,7 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(),
}));
jest.mock('../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
const IntlRegistrationPage = injectIntl(RegistrationPage);
const mockStore = configureStore();
@@ -120,6 +124,7 @@ describe('ThirdPartyAuth', () => {
institutionLogin: false,
};
window.location = { search: '' };
useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
});
afterEach(() => {

View File

@@ -8,7 +8,7 @@ export const REGISTRATION_CLEAR_BACKEND_ERROR = 'REGISTRATION_CLEAR_BACKEND_ERRO
export const REGISTER_SET_COUNTRY_CODE = 'REGISTER_SET_COUNTRY_CODE';
export const REGISTER_SET_USER_PIPELINE_DATA_LOADED = 'REGISTER_SET_USER_PIPELINE_DATA_LOADED';
export const REGISTER_SET_EMAIL_SUGGESTIONS = 'REGISTER_SET_EMAIL_SUGGESTIONS';
export const REGISTER_SET_AUTO_GENERATED_USERNAME_REGISTRATION_EXP_DATA = 'REGISTER_SET_AUTO_GENERATED_USERNAME_REGISTRATION_EXP_DATA';
// Backup registration form
export const backupRegistrationForm = () => ({
type: BACKUP_REGISTRATION_DATA.BASE,
@@ -83,3 +83,9 @@ export const setUserPipelineDataLoaded = (value) => ({
type: REGISTER_SET_USER_PIPELINE_DATA_LOADED,
payload: { value },
});
// Auto Generated Username Registration Experiment Actions
export const setAutoGeneratedUsernameExperimentData = (autoGeneratedRegExpVariation) => ({
type: REGISTER_SET_AUTO_GENERATED_USERNAME_REGISTRATION_EXP_DATA,
payload: { autoGeneratedRegExpVariation },
});

View File

@@ -0,0 +1,30 @@
/**
* This file contains data for auto generated username Optimizely experiment
*/
import { getConfig } from '@edx/frontend-platform';
export const NOT_INITIALIZED = 'experiment-not-initialized';
export const CONTROL = 'control-registration-page';
export const AUTO_GENERATED_USERNAME_REGISTRATION_EXP_VARIATION = 'auto-generated-username-register-page';
const AUTO_GENERATED_USERNAME_EXP_PAGE = 'targeting_for_auto_generated_username_page';
export function getAutoGeneratedUsernameExperimentVariation() {
try {
if (window.optimizely
&& window.optimizely.get('data').experiments[getConfig().AUTO_GENERATED_USERNAME_EXPERIMENT_ID]) {
const selectedVariant = window.optimizely.get('state').getVariationMap()[
getConfig().AUTO_GENERATED_USERNAME_EXPERIMENT_ID
];
return selectedVariant?.name;
}
} catch (e) { /* empty */ }
return '';
}
export function activateAutoGeneratedUsernameExperiment() {
window.optimizely = window.optimizely || [];
window.optimizely.push({
type: 'page',
pageName: AUTO_GENERATED_USERNAME_EXP_PAGE,
});
}

View File

@@ -0,0 +1,53 @@
import { useEffect, useState } from 'react';
import {
activateAutoGeneratedUsernameExperiment,
getAutoGeneratedUsernameExperimentVariation,
NOT_INITIALIZED,
} from './helper';
import { COMPLETE_STATE } from '../../../data/constants';
/**
* This hook returns activates multi step registration experiment and returns the experiment
* variation for the user.
*/
const useAutoGeneratedUsernameExperimentVariation = (
initExpVariation,
registrationEmbedded,
tpaHint,
currentProvider,
thirdPartyAuthApiStatus,
) => {
const [variation, setVariation] = useState(initExpVariation);
useEffect(() => {
if (initExpVariation || registrationEmbedded || !!tpaHint || !!currentProvider
|| thirdPartyAuthApiStatus !== COMPLETE_STATE) {
return variation;
}
const getVariation = () => {
const expVariation = getAutoGeneratedUsernameExperimentVariation();
if (expVariation) {
setVariation(expVariation);
} else {
// This is to handle the case when user dont get variation for some reason, the register page
// shows unlimited spinner.
setVariation(NOT_INITIALIZED);
}
};
activateAutoGeneratedUsernameExperiment();
const timer = setTimeout(getVariation, 300);
return () => {
clearTimeout(timer);
};
}, [ // eslint-disable-line react-hooks/exhaustive-deps
initExpVariation, currentProvider, registrationEmbedded, thirdPartyAuthApiStatus, tpaHint,
]);
return variation;
};
export default useAutoGeneratedUsernameExperimentVariation;

View File

@@ -3,6 +3,7 @@ import {
REGISTER_CLEAR_USERNAME_SUGGESTIONS,
REGISTER_FORM_VALIDATIONS,
REGISTER_NEW_USER,
REGISTER_SET_AUTO_GENERATED_USERNAME_REGISTRATION_EXP_DATA,
REGISTER_SET_COUNTRY_CODE,
REGISTER_SET_EMAIL_SUGGESTIONS,
REGISTER_SET_USER_PIPELINE_DATA_LOADED,
@@ -39,6 +40,7 @@ export const defaultState = {
usernameSuggestions: [],
validationApiRateLimited: false,
shouldBackupState: false,
autoGeneratedUsernameExperimentVariation: '',
};
const reducer = (state = defaultState, action = {}) => {
@@ -55,6 +57,12 @@ const reducer = (state = defaultState, action = {}) => {
registrationFormData: { ...action.payload },
userPipelineDataLoaded: state.userPipelineDataLoaded,
};
case REGISTER_SET_AUTO_GENERATED_USERNAME_REGISTRATION_EXP_DATA: {
return {
...state,
autoGeneratedUsernameExperimentVariation: action.payload.autoGeneratedRegExpVariation,
};
}
case REGISTER_NEW_USER.BEGIN:
return {
...state,

View File

@@ -1,14 +1,11 @@
import { camelCaseObject } from '@edx/frontend-platform';
import { logError, logInfo } from '@edx/frontend-platform/logging';
import {
call, put, race, take, takeEvery,
} from 'redux-saga/effects';
import { call, put, takeEvery } from 'redux-saga/effects';
import {
fetchRealtimeValidationsBegin,
fetchRealtimeValidationsFailure,
fetchRealtimeValidationsSuccess,
REGISTER_CLEAR_USERNAME_SUGGESTIONS,
REGISTER_FORM_VALIDATIONS,
REGISTER_NEW_USER,
registerNewUserBegin,
@@ -44,15 +41,9 @@ export function* handleNewUserRegistration(action) {
export function* fetchRealtimeValidations(action) {
try {
yield put(fetchRealtimeValidationsBegin());
const { fieldValidations } = yield call(getFieldsValidations, action.payload.formPayload);
const { response } = yield race({
response: call(getFieldsValidations, action.payload.formPayload),
cancel: take(REGISTER_CLEAR_USERNAME_SUGGESTIONS),
});
if (response) {
yield put(fetchRealtimeValidationsSuccess(camelCaseObject(response.fieldValidations)));
}
yield put(fetchRealtimeValidationsSuccess(camelCaseObject(fieldValidations)));
} catch (e) {
if (e.response && e.response.status === 403) {
yield put(fetchRealtimeValidationsFailure());

View File

@@ -38,6 +38,7 @@ describe('Registration Reducer Tests', () => {
usernameSuggestions: [],
validationApiRateLimited: false,
shouldBackupState: false,
autoGeneratedUsernameExperimentVariation: '',
};
it('should return the initial state', () => {

View File

@@ -59,6 +59,7 @@ export const isFormValid = (
isValid = false;
}
emailSuggestion = suggestion;
if (fieldErrors.email) { isValid = false; }
break;
}
case 'username':

View File

@@ -18,7 +18,7 @@ import { useNavigate, useParams } from 'react-router-dom';
import { resetPassword, validateToken } from './data/actions';
import {
FORM_SUBMISSION_ERROR, PASSWORD_RESET_ERROR, PASSWORD_VALIDATION_ERROR, TOKEN_STATE,
FORM_SUBMISSION_ERROR, PASSWORD_RESET_ERROR, PASSWORD_VALIDATION_ERROR, SUCCESS, TOKEN_STATE,
} from './data/constants';
import { resetPasswordResultSelector } from './data/selectors';
import { validatePassword } from './data/service';
@@ -30,6 +30,7 @@ import {
LETTER_REGEX, LOGIN_PAGE, NUMBER_REGEX, RESET_PAGE,
} from '../data/constants';
import { getAllPossibleQueryParams, updatePathWithQueryParams, windowScrollTo } from '../data/utils';
import { trackPasswordResetSuccess, trackResetPasswordPageViewed } from '../tracking/trackers/reset-password';
const ResetPasswordPage = (props) => {
const { formatMessage } = useIntl();
@@ -42,6 +43,15 @@ const ResetPasswordPage = (props) => {
const { token } = useParams();
const navigate = useNavigate();
useEffect(() => {
if (props.status === TOKEN_STATE.VALID) {
trackResetPasswordPageViewed();
}
if (props.status === SUCCESS) {
trackPasswordResetSuccess();
}
}, [props.status]);
useEffect(() => {
if (props.status !== TOKEN_STATE.PENDING && props.status !== PASSWORD_RESET_ERROR) {
setErrorCode(props.status);
@@ -139,7 +149,7 @@ const ResetPasswordPage = (props) => {
}
} else if (props.status === PASSWORD_RESET_ERROR) {
navigate(updatePathWithQueryParams(RESET_PAGE));
} else if (props.status === 'success') {
} else if (props.status === SUCCESS) {
navigate(updatePathWithQueryParams(LOGIN_PAGE));
} else {
return (

View File

@@ -19,6 +19,11 @@ import ResetPasswordPage from '../ResetPasswordPage';
const mockedNavigator = jest.fn();
const token = '1c-bmjdkc-5e60e084cf8113048ca7';
jest.mock('@edx/frontend-platform/analytics', () => ({
sendPageEvent: jest.fn(),
sendTrackEvent: jest.fn(),
}));
jest.mock('@edx/frontend-platform/auth');
jest.mock('react-router-dom', () => ({
...(jest.requireActual('react-router-dom')),

View File

@@ -65,15 +65,10 @@
margin-right: 0.25rem;
}
.username__form-group-wrapper {
position: relative;
}
.username-suggestions {
position: absolute;
inset: 0;
padding-left: 15px;
z-index: 100;
position: relative;
margin-top: -2.5rem;
margin-left: 15px;
}
.username-suggestions__close__button {
@@ -81,6 +76,13 @@
position: absolute;
}
.username-suggestions__error {
position: relative;
margin-top: -13.7%;
margin-bottom: 11%;
margin-left: 15px;
}
.username-scroll-suggested--form-field {
width: 20rem;
white-space: nowrap;

View File

@@ -0,0 +1,22 @@
import { createEventTracker, createPageEventTracker } from '../../data/segment/utils';
export const eventNames = {
loginAndRegistration: 'login_and_registration',
forgotPasswordPageViewed: 'edx.bi.password_reset_form.viewed',
};
export const categories = {
userEngagement: 'user-engagement',
};
// Event tracker for forgot password page viewed
export const trackForgotPasswordPageViewed = () => createEventTracker(
eventNames.forgotPasswordPageViewed,
{
category: categories.userEngagement,
},
)();
export const trackForgotPasswordPageEvent = () => {
createPageEventTracker(eventNames.loginAndRegistration, 'forgot-password')();
};

View File

@@ -0,0 +1,29 @@
import { createEventTracker, createPageEventTracker } from '../../data/segment/utils';
export const eventNames = {
forgotPasswordLinkClicked: 'edx.bi.password-reset_form.toggled',
loginAndRegistration: 'login_and_registration',
registerFormToggled: 'edx.bi.register_form.toggled',
loginSuccess: 'edx.bi.user.account.authenticated.client',
};
export const categories = {
userEngagement: 'user-engagement',
};
// Event tracker for Forgot Password link click
export const trackForgotPasswordLinkClick = () => createEventTracker(
eventNames.forgotPasswordLinkClicked,
{ category: categories.userEngagement },
)();
// Tracks the login page event.
export const trackLoginPageViewed = () => {
createPageEventTracker(eventNames.loginAndRegistration, 'login')();
};
// Tracks the login sucess event.
export const trackLoginSuccess = () => createEventTracker(
eventNames.loginSuccess,
{},
)();

View File

@@ -0,0 +1,37 @@
import { createEventTracker, createPageEventTracker } from '../../data/segment/utils';
export const eventNames = {
progressiveProfilingSubmitClick: 'edx.bi.welcome.page.submit.clicked',
progressiveProfilingSkipLinkClick: 'edx.bi.welcome.page.skip.link.clicked',
disablePostRegistrationRecommendations: 'edx.bi.user.recommendations.not.enabled',
progressiveProfilingSupportLinkCLick: 'edx.bi.welcome.page.support.link.clicked',
loginAndRegistration: 'login_and_registration',
};
// Event link tracker for Progressive profiling skip button click
export const trackProgressiveProfilingSkipLinkClick = evenProperties => createEventTracker(
eventNames.progressiveProfilingSkipLinkClick, { ...evenProperties },
)();
// Event tracker for progressive profiling submit button click
export const trackProgressiveProfilingSubmitClick = (evenProperties) => createEventTracker(
eventNames.progressiveProfilingSubmitClick,
{ ...evenProperties },
)();
// Event tracker for progressive profiling submit button click
export const trackDisablePostRegistrationRecommendations = (evenProperties) => createEventTracker(
eventNames.disablePostRegistrationRecommendations,
{ ...evenProperties },
)();
// Tracks the progressive profiling page event.
export const trackProgressiveProfilingPageViewed = () => {
createPageEventTracker(eventNames.loginAndRegistration, 'welcome')();
};
// Tracks the progressive profiling spport link click.
export const trackProgressiveProfilingSupportLinkCLick = () => createEventTracker(
eventNames.progressiveProfilingSupportLinkCLick,
{},
)();

View File

@@ -0,0 +1,22 @@
import { createEventTracker, createPageEventTracker } from '../../data/segment/utils';
export const eventNames = {
loginAndRegistration: 'login_and_registration',
registrationSuccess: 'edx.bi.user.account.registered.client',
loginFormToggled: 'edx.bi.login_form.toggled',
};
export const categories = {
userEngagement: 'user-engagement',
};
// Event tracker for successful registration
export const trackRegistrationSuccess = () => createEventTracker(
eventNames.registrationSuccess,
{},
)();
// Tracks the progressive profiling page event.
export const trackRegistrationPageViewed = () => {
createPageEventTracker(eventNames.loginAndRegistration, 'register')();
};

View File

@@ -0,0 +1,14 @@
import { createEventTracker, createPageEventTracker } from '../../data/segment/utils';
export const eventNames = {
loginAndRegistration: 'login_and_registration',
resetPasswordSuccess: 'edx.bi.user.password.reset.success',
};
export const trackResetPasswordPageViewed = () => {
createPageEventTracker(eventNames.loginAndRegistration, 'reset-password')();
};
export const trackPasswordResetSuccess = () => {
createEventTracker(eventNames.resetPasswordSuccess, {})();
};

View File

@@ -0,0 +1,37 @@
import { createEventTracker, createPageEventTracker } from '../../../data/segment/utils';
import {
categories,
eventNames,
trackForgotPasswordPageEvent,
trackForgotPasswordPageViewed,
} from '../forgotpassword';
// Mock createEventTracker function
jest.mock('../../../data/segment/utils', () => ({
createEventTracker: jest.fn().mockImplementation(() => jest.fn()),
createPageEventTracker: jest.fn().mockImplementation(() => jest.fn()),
}));
describe('Tracking Functions', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should fire trackForgotPasswordPageEvent', () => {
trackForgotPasswordPageEvent();
expect(createPageEventTracker).toHaveBeenCalledWith(
eventNames.loginAndRegistration,
'forgot-password',
);
});
it('should fire forgotPasswordPageViewedEvent', () => {
trackForgotPasswordPageViewed();
expect(createEventTracker).toHaveBeenCalledWith(
eventNames.forgotPasswordPageViewed,
{ category: categories.userEngagement },
);
});
});

View File

@@ -0,0 +1,37 @@
import { createEventTracker, createPageEventTracker } from '../../../data/segment/utils';
import {
categories,
eventNames,
trackForgotPasswordLinkClick,
trackLoginPageViewed,
} from '../login';
// Mock createEventTracker function
jest.mock('../../../data/segment/utils', () => ({
createEventTracker: jest.fn().mockImplementation(() => jest.fn()),
createPageEventTracker: jest.fn().mockImplementation(() => jest.fn()),
}));
describe('Tracking Functions', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('trackForgotPasswordLinkClick function', () => {
trackForgotPasswordLinkClick();
expect(createEventTracker).toHaveBeenCalledWith(
eventNames.forgotPasswordLinkClicked,
{ category: categories.userEngagement },
);
});
it('trackLoginPageEvent function', () => {
trackLoginPageViewed();
expect(createPageEventTracker).toHaveBeenCalledWith(
eventNames.loginAndRegistration,
'login',
);
});
});

View File

@@ -0,0 +1,37 @@
import { createEventTracker, createPageEventTracker } from '../../../data/segment/utils';
import {
eventNames,
trackProgressiveProfilingPageViewed,
trackProgressiveProfilingSkipLinkClick,
} from '../progressive-profiling';
// Mock createEventTracker function
jest.mock('../../../data/segment/utils', () => ({
createEventTracker: jest.fn().mockImplementation(() => jest.fn()),
createPageEventTracker: jest.fn().mockImplementation(() => jest.fn()),
createLinkTracker: jest.fn().mockImplementation(() => jest.fn()),
}));
describe('Tracking Functions', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should fire trackProgressiveProfilingSkipLinkClickEvent', () => {
trackProgressiveProfilingSkipLinkClick();
expect(createEventTracker).toHaveBeenCalledWith(
eventNames.progressiveProfilingSkipLinkClick,
{},
);
});
it('should fire trackProgressiveProfilingPageEvent', () => {
trackProgressiveProfilingPageViewed();
expect(createPageEventTracker).toHaveBeenCalledWith(
eventNames.loginAndRegistration,
'welcome',
);
});
});

View File

@@ -0,0 +1,36 @@
import { createEventTracker, createPageEventTracker } from '../../../data/segment/utils';
import {
eventNames,
trackRegistrationPageViewed,
trackRegistrationSuccess,
} from '../register';
// Mock createEventTracker function
jest.mock('../../../data/segment/utils', () => ({
createEventTracker: jest.fn().mockImplementation(() => jest.fn()),
createPageEventTracker: jest.fn().mockImplementation(() => jest.fn()),
}));
describe('Tracking Functions', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should fire registrationSuccessEvent', () => {
trackRegistrationSuccess();
expect(createEventTracker).toHaveBeenCalledWith(
eventNames.registrationSuccess,
{},
);
});
it('should fire trackRegistrationPageEvent', () => {
trackRegistrationPageViewed();
expect(createPageEventTracker).toHaveBeenCalledWith(
eventNames.loginAndRegistration,
'register',
);
});
});

View File

@@ -0,0 +1,26 @@
import { createPageEventTracker } from '../../../data/segment/utils';
import {
eventNames,
trackResetPasswordPageViewed,
} from '../reset-password';
// Mock createEventTracker function
jest.mock('../../../data/segment/utils', () => ({
createEventTracker: jest.fn().mockImplementation(() => jest.fn()),
createPageEventTracker: jest.fn().mockImplementation(() => jest.fn()),
}));
describe('Tracking Functions', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should fire trackResettPasswordPageEvent', () => {
trackResetPasswordPageViewed();
expect(createPageEventTracker).toHaveBeenCalledWith(
eventNames.loginAndRegistration,
'reset-password',
);
});
});