Compare commits

..

1 Commits

Author SHA1 Message Date
Kyr
116830d2c9 fix: username suggestions alignment (#1279)
Co-authored-by: Kyrylo Hudym-Levkovych <kyr.hudym@kyrs-MacBook-Pro.local>
2025-08-08 16:55:59 +05:00
98 changed files with 10707 additions and 6092 deletions

3
.env
View File

@@ -16,9 +16,6 @@ SITE_NAME=null
INFO_EMAIL=''
# ***** Cookies *****
USER_RETENTION_COOKIE_NAME=null
# ***** Cohesion Keys *****
COHESION_WRITE_KEY=''
COHESION_SOURCE_KEY=''
# ***** Links *****
LOGIN_ISSUE_SUPPORT_LINK=''
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK=null

View File

@@ -25,9 +25,6 @@ ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN='true'
# ***** Cookies *****
SESSION_COOKIE_DOMAIN='localhost'
USER_INFO_COOKIE_NAME='edx-user-info'
# ***** Cohesion Keys *****
COHESION_WRITE_KEY=''
COHESION_SOURCE_KEY=''
# ***** Links *****
LOGIN_ISSUE_SUPPORT_LINK='http://localhost:18000/login-issue-support-url'
TOS_AND_HONOR_CODE='http://localhost:18000/honor'

View File

@@ -18,5 +18,3 @@ SEGMENT_KEY=''
SITE_NAME='Your Platform Name Here'
APP_ID=''
MFE_CONFIG_API_URL=''
COHESION_WRITE_KEY=''
COHESION_SOURCE_KEY=''

View File

@@ -1,7 +1,7 @@
// eslint-disable-next-line import/no-extraneous-dependencies
const { createConfig } = require('@openedx/frontend-build');
const config = createConfig('eslint', {
module.exports = createConfig('eslint', {
rules: {
// Temporarily update the 'indent', 'template-curly-spacing' and
// 'no-multiple-empty-lines' rules since they are causing eslint
@@ -50,14 +50,3 @@ const config = createConfig('eslint', {
'function-paren-newline': 'off',
},
});
config.settings = {
'import/resolver': {
node: {
paths: ['src', 'node_modules'],
extensions: ['.js', '.jsx'],
},
},
};
module.exports = config;

1
.github/CODEOWNERS vendored
View File

@@ -1 +0,0 @@
* @openedx/2U-infinity

View File

@@ -1,7 +0,0 @@
version: 2
updates:
# Adding new check for github-actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

View File

@@ -25,5 +25,5 @@ Include a link to the sandbox for design changes or screenshot for before and af
#### Post-merge Checklist
* [ ] Deploy the changes to prod after verifying on stage or ask **@openedx/2u-infinity** to do it.
* [ ] Deploy the changes to prod after verifying on stage or ask **@openedx/2u-vanguards** to do it.
* [ ] 🎉 🙌 Celebrate! Thanks for your contribution.

View File

@@ -10,7 +10,7 @@ on:
jobs:
autoupdate:
name: autoupdate
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: docker://chinthakagodawita/autoupdate-action:v1
env:

View File

@@ -10,15 +10,17 @@ on:
jobs:
tests:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
- name: Setup Nodejs
uses: actions/setup-node@v4
uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
node-version: ${{ env.NODE_VER }}
- name: Install Dependencies
run: npm ci
@@ -39,7 +41,4 @@ jobs:
run: npm run build
- name: Run Code Coverage
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
uses: codecov/codecov-action@v3

View File

@@ -10,4 +10,4 @@ on:
jobs:
version-check:
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master
uses: openedx/.github/.github/workflows/lockfile-check.yml@master

2
.nvmrc
View File

@@ -1 +1 @@
20
18

View File

@@ -1,2 +1,2 @@
# The following users are the owners of all frontend-app-authn files
* @openedx/2u-infinity
* @openedx/2u-vanguards

View File

@@ -187,7 +187,7 @@ All community members are expected to follow the `Open edX Code of Conduct <http
People
======
The assigned maintainers for this component and other project details may be
found in `Backstage <https://backstage.openedx.org/catalog/default/group/2u-infinity>`_. Backstage pulls this data from the ``catalog-info.yaml``
found in `Backstage <https://backstage.openedx.org/catalog/default/group/2u-vanguards>`_. Backstage pulls this data from the ``catalog-info.yaml``
file in this repo.
Reporting Security Issues

View File

@@ -13,6 +13,6 @@ metadata:
annotations:
openedx.org/arch-interest-groups: ""
spec:
owner: group:2u-infinity
owner: group:2u-vanguards
type: 'service'
lifecycle: 'production'

View File

@@ -1,60 +0,0 @@
/*
Authn MFE is now able to handle JS-based configuration!
For the time being, the `.env.*` files are still made available when cloning down this repo or pulling from
the master branch. To switch to using `env.config.js`, make a copy of `example.env.config.js` and configure as needed.
For testing with Jest Snapshot, there is a mock in `/src/setupTest.jsx` for `getConfig` that will need to be
uncommented.
Note: having both .env and env.config.js files will follow a predictable order, in which non-empty values in the
JS-based config will overwrite the .env environment variables.
frontend-platform's getConfig loads configuration in the following sequence:
- .env file config
- optional handlers (commonly used to merge MFE-specific config in via additional process.env variables)
- env.config.js file config
- runtime config
*/
module.exports = {
NODE_ENV: 'development',
NODE_PATH: './src',
PORT: 1999,
ACCESS_TOKEN_COOKIE_NAME: 'edx-jwt-cookie-header-payload',
BASE_URL: 'http://localhost:1999',
CREDENTIALS_BASE_URL: 'http://localhost:18150',
CSRF_TOKEN_API_PATH: '/csrf/api/v1/token',
ECOMMERCE_BASE_URL: 'http://localhost:18130',
LANGUAGE_PREFERENCE_COOKIE_NAME: 'openedx-language-preference',
LMS_BASE_URL: 'http://localhost:18000',
LOGIN_URL: 'http://localhost:1999/login',
LOGOUT_URL: 'http://localhost:18000/logout',
LOGO_URL: 'https://edx-cdn.org/v3/default/logo.svg',
LOGO_TRADEMARK_URL: 'https://edx-cdn.org/v3/default/logo-trademark.svg',
LOGO_WHITE_URL: 'https://edx-cdn.org/v3/default/logo-white.svg',
FAVICON_URL: 'https://edx-cdn.org/v3/default/favicon.ico',
MARKETING_SITE_BASE_URL: 'http://localhost:18000',
ORDER_HISTORY_URL: 'http://localhost:1996/orders',
REFRESH_ACCESS_TOKEN_ENDPOINT: 'http://localhost:18000/login_refresh',
SEGMENT_KEY: '',
SITE_NAME: 'Your Platform Name Here',
INFO_EMAIL: 'info@example.com',
ENABLE_DYNAMIC_REGISTRATION_FIELDS: 'true',
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN: 'true',
SESSION_COOKIE_DOMAIN: 'localhost',
USER_INFO_COOKIE_NAME: 'edx-user-info',
LOGIN_ISSUE_SUPPORT_LINK: 'http://localhost:18000/login-issue-support-url',
TOS_AND_HONOR_CODE: 'http://localhost:18000/honor',
TOS_LINK: 'http://localhost:18000/tos',
PRIVACY_POLICY: 'http://localhost:18000/privacy',
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: 'http://localhost:1999/welcome',
BANNER_IMAGE_LARGE: '',
BANNER_IMAGE_MEDIUM: '',
BANNER_IMAGE_SMALL: '',
BANNER_IMAGE_EXTRA_SMALL: '',
APP_ID: '',
MFE_CONFIG_API_URL: '',
ZENDESK_KEY: '',
ZENDESK_LOGO_URL: '',
};

View File

@@ -3,6 +3,6 @@
nick: Authn MFE
oeps: {}
owner: openedx/2u-infinity
owner: openedx/2u-vanguards
openedx-release:
ref: master

14762
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -33,13 +33,12 @@
},
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-platform": "^8.0.0",
"@edx/frontend-platform": "7.1.3",
"@edx/openedx-atlas": "^0.6.0",
"@fortawesome/fontawesome-svg-core": "6.7.1",
"@fortawesome/free-brands-svg-icons": "6.7.1",
"@fortawesome/free-solid-svg-icons": "6.7.1",
"@fortawesome/react-fontawesome": "0.2.2",
"@openedx/frontend-plugin-framework": "^1.3.0",
"@fortawesome/fontawesome-svg-core": "6.5.2",
"@fortawesome/free-brands-svg-icons": "6.5.2",
"@fortawesome/free-solid-svg-icons": "6.5.2",
"@fortawesome/react-fontawesome": "0.2.0",
"@openedx/paragon": "^22.1.1",
"@optimizely/react-sdk": "^2.9.1",
"@redux-devtools/extension": "3.3.0",
@@ -48,39 +47,38 @@
"algoliasearch": "^4.14.3",
"algoliasearch-helper": "^3.14.0",
"classnames": "2.5.1",
"core-js": "3.39.0",
"core-js": "3.36.1",
"fastest-levenshtein": "1.0.16",
"form-urlencoded": "6.1.5",
"form-urlencoded": "6.1.4",
"prop-types": "15.8.1",
"query-string": "7.1.3",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-error-boundary": "^4.0.13",
"react-helmet": "6.1.0",
"react-loading-skeleton": "3.5.0",
"react-loading-skeleton": "3.4.0",
"react-redux": "7.2.9",
"react-responsive": "8.2.0",
"react-router": "6.28.0",
"react-router-dom": "6.28.0",
"react-router": "6.22.3",
"react-router-dom": "6.22.3",
"react-zendesk": "^0.1.13",
"redux": "4.2.1",
"redux": "4.2.0",
"redux-logger": "3.0.6",
"redux-mock-store": "1.5.5",
"redux-mock-store": "1.5.4",
"redux-saga": "1.3.0",
"redux-thunk": "2.4.2",
"regenerator-runtime": "0.14.1",
"reselect": "5.1.1",
"universal-cookie": "7.2.2"
"reselect": "4.1.8",
"universal-cookie": "4.0.4"
},
"devDependencies": {
"@edx/browserslist-config": "^1.1.1",
"@edx/reactifex": "1.1.0",
"@openedx/frontend-build": "^14.0.3",
"babel-plugin-formatjs": "10.5.26",
"eslint-plugin-import": "2.31.0",
"@openedx/frontend-build": "13.1.4",
"babel-plugin-formatjs": "10.5.14",
"eslint-plugin-import": "2.29.1",
"glob": "7.2.3",
"history": "5.3.0",
"husky": "9.1.7",
"husky": "7.0.4",
"jest": "29.7.0",
"react-test-renderer": "^17.0.2"
}

View File

@@ -1,30 +1,25 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en-us">
<head>
<title>
<%= (process.env.SITE_NAME && process.env.SITE_NAME !='null' ) ?
'Authentication | ' + process.env.SITE_NAME : 'Authentication' %>
</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
rel="shortcut icon"
href="<%=htmlWebpackPlugin.options.FAVICON_URL%>"
type="image/x-icon"
/>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.4.4/iframeResizer.contentWindow.min.js"
integrity="sha512-IWwZFBvHzN41wNI6etRLLuLrDDj/6AwJcPt7cmKJAzluYTIHHQ1PF8wh0rSy05jxEvvjflVvH2MxeV6riyEEXg=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
></script>
<% if (process.env.OPTIMIZELY_URL) { %>
<script src="<%= process.env.OPTIMIZELY_URL %>"></script>
<% } else if (process.env.OPTIMIZELY_PROJECT_ID) { %>
<script src="<%= process.env.MARKETING_SITE_BASE_URL %>/optimizelyjs/<%= process.env.OPTIMIZELY_PROJECT_ID %>.js"></script>
<% } %>
<title><%= (process.env.SITE_NAME && process.env.SITE_NAME != 'null') ? 'Authentication | ' + process.env.SITE_NAME : 'Authentication' %></title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.3.9/iframeResizer.contentWindow.min.js"
integrity="sha512-mdT/HQRzoRP4laVz49Mndx6rcCGA3IhuyhP3gaY0E9sZPkwbtDk9ttQIq9o8qGCf5VvJv1Xsy3k2yTjfUoczqw=="
crossorigin="anonymous"
referrerpolicy="no-referrer">
</script>
<% if (process.env.OPTIMIZELY_URL) { %>
<script
src="<%= process.env.OPTIMIZELY_URL %>"
></script>
<% } else if (process.env.OPTIMIZELY_PROJECT_ID) { %>
<script
src="<%= process.env.MARKETING_SITE_BASE_URL %>/optimizelyjs/<%= process.env.OPTIMIZELY_PROJECT_ID %>.js"
></script>
<% } %>
</head>
<body>
<div id="root"></div>
</body>

View File

@@ -6,7 +6,7 @@ import { Helmet } from 'react-helmet';
import { Navigate, Route, Routes } from 'react-router-dom';
import {
EmbeddedRegistrationRoute, NotFoundPage, registerIcons, UnAuthOnlyRoute,
EmbeddedRegistrationRoute, NotFoundPage, registerIcons, UnAuthOnlyRoute, Zendesk,
} from './common-components';
import configureStore from './data/configureStore';
import {
@@ -22,90 +22,21 @@ import {
import { updatePathWithQueryParams } from './data/utils';
import { ForgotPasswordPage } from './forgot-password';
import Logistration from './logistration/Logistration';
import MainAppSlot from './plugin-slots/MainAppSlot';
import { ProgressiveProfiling } from './progressive-profiling';
import { RecommendationsPage } from './recommendations';
import { RegistrationPage } from './register';
import { ResetPasswordPage } from './reset-password';
import './index.scss';
import './cohesion/cohesion.scss';
registerIcons();
const MainApp = () => (
<AppProvider store={configureStore()}>
<Helmet>
{/* Cohesion Snippet */}
<script>
{`!function (co, h, e, s, i, o, n) {
var d = 'documentElement'; var a = 'className'; h[d][a] += ' preampjs fusejs';
n.k = e; co._Cohesion = n; co._Preamp = { k: s, start: new Date }; co._Fuse = { k: i }; co._Tagular = { k: o };
[e, s, i, o].map(function (x) { co[x] = co[x] || function () { (co[x].q = co[x].q || []).push([].slice.call(arguments)) } });
var b = function () { var u = h[d][a]; h[d][a] = u.replace(/ ?preampjs| ?fusejs/g, '') };
h.addEventListener('DOMContentLoaded', function () {
co.setTimeout(b, 3e3);
co._Preamp.docReady = co._Fuse.docReady = !0
}); var z = h.createElement('script');
z.async = 1; z.src = 'https://beam.edx.org/cohesion/cohesion-latest.min.js';
z.onerror = function () { var ce = 'error',f = 'function'; for (var o of co[e].q || []) o[0] === ce && typeof o[1] == f && o[1](); co[e] = function (n, cb) { n === ce && typeof cb == f && cb() }; b() };
h.head.appendChild(z);
}
(window, document, 'cohesion', 'preamp', 'fuse', 'tagular', {
tagular: {
apiHost: 'https://beam.edx.org/v2/t',
writeKey: "${process.env.COHESION_WRITE_KEY}",
sourceKey: "${process.env.COHESION_SOURCE_KEY}",
cookieDomain: 'edx.org',
domainWhitelist: ["edx.org"],
apiVersion: 'v2/t',
multiparty: true,
useBeacon: true,
},
consent: {
onetrust: {
enabled: true,
optIn: true
},
required: true,
domain: '.edx.org',
},
});`}
</script>
{/* Stitch Cohesion and Segment IDs */}
<script>
{`cohesion("tagular:ready", function () {
window.analytics.ready(function () {
const cohesionAnonymId = window.tagular("getAliasSet")["anonymousId"];
const segmentAnonymId = window.analytics.user().anonymousId();
const segmentUserId = window.analytics.user().id();
window.analytics.identify(segmentUserId, {
cohesion_anonymous_id: cohesionAnonymId,
});
window.tagular("beam", {
"@type": "core.Identify.v1",
traits: {},
externalIds: [
{
id: segmentAnonymId,
type: "segment_anonymous_id",
collection: "users",
encoding: "none",
},
{
id: cohesionAnonymId,
type: "cohesion_anonymous_id",
collection: "users",
encoding: "none",
},
],
});
});
});`}
</script>
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
</Helmet>
{getConfig().ZENDESK_KEY && <Zendesk />}
<Routes>
<Route path="/" element={<Navigate replace to={updatePathWithQueryParams(REGISTER_PAGE)} />} />
<Route
@@ -126,7 +57,6 @@ const MainApp = () => (
<Route path={PAGE_NOT_FOUND} element={<NotFoundPage />} />
<Route path="*" element={<Navigate replace to={PAGE_NOT_FOUND} />} />
</Routes>
<MainAppSlot />
</AppProvider>
);

View File

@@ -1,4 +0,0 @@
.preampjs [data-preamp],
.fusejs [data-fuse] {
opacity: 0 !important;
}

View File

@@ -1,22 +0,0 @@
export const PAGE_TYPES = {
ACCOUNT_CREATION: 'account-creation',
SIGN_IN: 'sign-in',
};
export const ELEMENT_TYPES = {
BUTTON: 'BUTTON',
};
export const EVENT_TYPES = { ElementClicked: 'redventures.usertracking.v3.ElementClicked' };
export const ELEMENT_TEXT = {
CREATE_ACCOUNT: 'create-account',
OPT_IN_TEXT: 'I agree that edx may send me marketing messages',
SIGN_IN: 'Sign In',
};
export const ELEMENT_NAME = {
SIGN_IN: PAGE_TYPES.SIGN_IN,
OPT_OUT: 'opt-out',
CREATE_ACCOUNT: 'Create an account for free',
};

View File

@@ -1,6 +0,0 @@
export const SET_COHESION_EVENT_ELEMENT_STATES = 'SET_COHESION_EVENT_ELEMENT_STATES';
export const setCohesionEventStates = (eventData) => ({
type: SET_COHESION_EVENT_ELEMENT_STATES,
payload: eventData,
});

View File

@@ -1,17 +0,0 @@
import { SET_COHESION_EVENT_ELEMENT_STATES } from './actions';
export const storeName = 'cohesion';
export const defaultState = {
eventData: {},
};
export const reducer = (state = defaultState, action = {}) => {
if (action.type === SET_COHESION_EVENT_ELEMENT_STATES) {
return {
...state,
eventData: action.payload,
};
}
return state;
};

View File

@@ -1,24 +0,0 @@
import { EVENT_TYPES } from './constants';
/**
* Tracks cohesion events by setting the page type and tracking a click event.
*
* @param {string} pageType - The type of page where the event occurred.
* @param {string} elementType - The type of the web element (e.g., 'BUTTON', 'LINK').
* @param {string} webElementText - The text content of the web element.
* @param {string} webElementName - The name of the web element.
*/
const trackCohesionEvent = (eventData) => {
window.chsn_pageType = eventData.pageType;
const webElement = {
elementType: eventData.elementType,
text: eventData.webElementText,
name: eventData.webElementName,
};
window.tagular('beam', {
'@type': EVENT_TYPES.ElementClicked,
webElement,
});
};
export default trackCohesionEvent;

View File

@@ -1,6 +0,0 @@
const mockTagular = () => {
const getTagular = jest.fn();
window.tagular = getTagular;
};
export default mockTagular;

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from 'react';
import React, { useState } from 'react';
import {
Form, TransitionReplace,
@@ -7,38 +7,6 @@ import PropTypes from 'prop-types';
const FormGroup = (props) => {
const [hasFocus, setHasFocus] = useState(false);
const [isAutoFill, setIsAutoFill] = useState(false);
const inputRef = useRef(null);
useEffect(() => {
const checkAutoFill = () => {
if (inputRef.current) {
// Check both standard and vendor-prefixed autofill
const isAutoFillField = inputRef.current.matches(
':autofill, :-webkit-autofill',
);
setIsAutoFill(isAutoFillField);
}
};
const observer = new MutationObserver(checkAutoFill);
if (inputRef.current) {
// Check immediately on mount
checkAutoFill();
// Configure observer to watch style changes only
observer.observe(inputRef.current, {
attributes: true,
attributeFilter: ['style'], // Autofill often changes styles
});
// Fallback check for browser timing issues
const timeoutId = setTimeout(checkAutoFill, 100);
return () => {
observer.disconnect();
clearTimeout(timeoutId);
};
}
return () => observer.disconnect();
}, []);
const handleFocus = (e) => {
setHasFocus(true);
@@ -55,7 +23,6 @@ const FormGroup = (props) => {
return (
<Form.Group controlId={props.name} className={props.className} isInvalid={props.errorMessage !== ''}>
<Form.Control
ref={inputRef}
as={props.as}
readOnly={props.readOnly}
type={props.type}
@@ -72,7 +39,6 @@ const FormGroup = (props) => {
controlClassName={props.borderClass}
trailingElement={props.trailingElement}
floatingLabel={props.floatingLabel}
isAutoFill={isAutoFill}
>
{props.options ? props.options() : null}
</Form.Control>

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

@@ -1,15 +1,11 @@
import { useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import PropTypes from 'prop-types';
import { Navigate } from 'react-router-dom';
import trackCohesionEvent from '../cohesion/trackers';
import {
AUTHN_PROGRESSIVE_PROFILING, RECOMMENDATIONS, REDIRECT,
} from '../data/constants';
import setCookie from '../data/utils/cookies';
import { redirectWithDelay } from '../data/utils/dataUtils';
import { setCookie } from '../data/utils';
const RedirectLogistration = (props) => {
const {
@@ -24,16 +20,10 @@ const RedirectLogistration = (props) => {
userId,
registrationEmbedded,
host,
currectProvider,
} = props;
const cohesionEventData = useSelector(state => state.cohesion.eventData);
let finalRedirectUrl = '';
if (success) {
// This event is used by cohesion upon successful login and registration
if (!currectProvider) {
trackCohesionEvent(cohesionEventData);
}
// If we're in a third party auth pipeline, we must complete the pipeline
// once user has successfully logged in. Otherwise, redirect to the specified redirect url.
// Note: For multiple enterprise use case, we need to make sure that user first visits the
@@ -85,7 +75,8 @@ const RedirectLogistration = (props) => {
/>
);
}
redirectWithDelay(finalRedirectUrl);
window.location.href = finalRedirectUrl;
}
return null;
@@ -103,7 +94,6 @@ RedirectLogistration.defaultProps = {
userId: null,
registrationEmbedded: false,
host: '',
currectProvider: '',
};
RedirectLogistration.propTypes = {
@@ -118,7 +108,6 @@ RedirectLogistration.propTypes = {
userId: PropTypes.number,
registrationEmbedded: PropTypes.bool,
host: PropTypes.string,
currectProvider: PropTypes.string,
};
export default RedirectLogistration;

View File

@@ -1,5 +1,4 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
@@ -9,35 +8,17 @@ import { Login } from '@openedx/paragon/icons';
import PropTypes from 'prop-types';
import messages from './messages';
import { ELEMENT_TYPES, PAGE_TYPES } from '../cohesion/constants';
import trackCohesionEvent from '../cohesion/trackers';
import {
LOGIN_PAGE, REGISTER_PAGE, SUPPORTED_ICON_CLASSES,
} from '../data/constants';
import { setCookie } from '../data/utils';
import { redirectWithDelay } from '../data/utils/dataUtils';
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
const SocialAuthProviders = (props) => {
const { formatMessage } = useIntl();
const { referrer, socialAuthProviders } = props;
const registrationFields = useSelector(state => state.register.registrationFormData);
function handleSubmit(e, providerName) {
function handleSubmit(e) {
e.preventDefault();
const eventData = {
pageType: referrer === LOGIN_PAGE ? PAGE_TYPES.SIGN_IN : PAGE_TYPES.ACCOUNT_CREATION,
elementType: ELEMENT_TYPES.BUTTON,
webElementText: providerName,
webElementName: providerName.toLowerCase(),
};
// This event is used by cohesion upon successful login
trackCohesionEvent(eventData);
if (referrer === REGISTER_PAGE) {
setCookie('marketingEmailsOptIn', registrationFields?.configurableFormFields?.marketingEmailsOptIn);
}
const url = e.currentTarget.dataset.providerUrl;
redirectWithDelay(getConfig().LMS_BASE_URL + url);
window.location.href = getConfig().LMS_BASE_URL + url;
}
const socialAuth = socialAuthProviders.map((provider, index) => (
@@ -47,7 +28,7 @@ const SocialAuthProviders = (props) => {
type="button"
className={`btn-social btn-${provider.id} ${index % 2 === 0 ? 'mr-3' : ''}`}
data-provider-url={referrer === LOGIN_PAGE ? provider.loginUrl : provider.registerUrl}
onClick={(event) => handleSubmit(event, provider?.name)}
onClick={handleSubmit}
>
{provider.iconImage ? (
<div aria-hidden="true">

View File

@@ -7,7 +7,6 @@ import PropTypes from 'prop-types';
import messages from './messages';
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
import setCookie from '../data/utils/cookies';
const ThirdPartyAuthAlert = (props) => {
const { formatMessage } = useIntl();
@@ -21,10 +20,7 @@ const ThirdPartyAuthAlert = (props) => {
message = formatMessage(messages['register.third.party.auth.account.not.linked'], { currentProvider, platformName });
}
if (currentProvider) {
// Setting this cookie to capture marketingEmailsOptIn for SSO flow on the onboarding component
setCookie('ssoPipelineRedirectionDone', true);
} else {
if (!currentProvider) {
return null;
}

View File

@@ -4,8 +4,9 @@ import { getConfig } from '@edx/frontend-platform';
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth';
import PropTypes from 'prop-types';
import { RESET_PAGE } from '../data/constants';
import { updatePathWithQueryParams } from '../data/utils';
import {
DEFAULT_REDIRECT_URL,
} from '../data/constants';
/**
* This wrapper redirects the requester to our default redirect url if they are
@@ -24,12 +25,7 @@ const UnAuthOnlyRoute = ({ children }) => {
if (isReady) {
if (authUser && authUser.username) {
const updatedPath = updatePathWithQueryParams(window.location.pathname);
if (updatedPath.startsWith(RESET_PAGE)) {
global.location.href = getConfig().LMS_BASE_URL;
return null;
}
global.location.href = getConfig().LMS_BASE_URL.concat(updatedPath);
global.location.href = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
return null;
}

View File

@@ -1,79 +0,0 @@
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,4 +1,3 @@
import { getConfig } from '@edx/frontend-platform';
import { logError } from '@edx/frontend-platform/logging';
import { call, put, takeEvery } from 'redux-saga/effects';
@@ -8,7 +7,6 @@ import {
getThirdPartyAuthContextSuccess,
THIRD_PARTY_AUTH_CONTEXT,
} from './actions';
import { progressiveProfilingFields, registerFields } from './constants';
import {
getThirdPartyAuthContext,
} from './service';
@@ -22,16 +20,7 @@ export function* fetchThirdPartyAuthContext(action) {
} = yield call(getThirdPartyAuthContext, action.payload.urlParams);
yield put(setCountryFromThirdPartyAuthContext(thirdPartyAuthContext.countryCode));
// 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));
}
yield put(getThirdPartyAuthContextSuccess(fieldDescriptions, optionalFields, thirdPartyAuthContext));
} catch (e) {
yield put(getThirdPartyAuthContextFailure());
logError(e);

View File

@@ -5,13 +5,14 @@ import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { render } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import {
MemoryRouter, Route, BrowserRouter as Router, Routes,
} from 'react-router-dom';
import { PAGE_NOT_FOUND, REGISTER_EMBEDDED_PAGE } from '../../data/constants';
import EmbeddedRegistrationRoute from '../EmbeddedRegistrationRoute';
import {
MemoryRouter, Route, BrowserRouter as Router, Routes,
} from 'react-router-dom';
const RRD = require('react-router-dom');
// Just render plain div with its children
// eslint-disable-next-line react/prop-types

View File

@@ -1,35 +1,16 @@
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',
@@ -49,11 +30,11 @@ describe('SocialAuthProviders', () => {
it('should match social auth provider with iconImage snapshot', () => {
props = { socialAuthProviders: [appleProvider, facebookProvider] };
const tree = renderer.create(reduxWrapper(
const tree = renderer.create(
<IntlProvider locale="en">
<SocialAuthProviders {...props} />
</IntlProvider>,
)).toJSON();
).toJSON();
expect(tree).toMatchSnapshot();
});
@@ -67,11 +48,11 @@ describe('SocialAuthProviders', () => {
}],
};
const tree = renderer.create(reduxWrapper(
const tree = renderer.create(
<IntlProvider locale="en">
<SocialAuthProviders {...props} />
</IntlProvider>,
)).toJSON();
).toJSON();
expect(tree).toMatchSnapshot();
});
@@ -85,11 +66,11 @@ describe('SocialAuthProviders', () => {
}],
};
const tree = renderer.create(reduxWrapper(
const tree = renderer.create(
<IntlProvider locale="en">
<SocialAuthProviders {...props} />
</IntlProvider>,
)).toJSON();
).toJSON();
expect(tree).toMatchSnapshot();
});

View File

@@ -5,13 +5,14 @@ import React from 'react';
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { render } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import {
MemoryRouter, Route, BrowserRouter as Router, Routes,
} from 'react-router-dom';
import { UnAuthOnlyRoute } from '..';
import { REGISTER_PAGE } from '../../data/constants';
import {
MemoryRouter, Route, BrowserRouter as Router, Routes,
} from 'react-router-dom';
jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedUser: jest.fn(),
fetchAuthenticatedUser: jest.fn(),

View File

@@ -66,14 +66,14 @@ exports[`SocialAuthProviders should match social auth provider with iconClass sn
data-prefix="fab"
focusable="false"
role="img"
style={{}}
style={Object {}}
viewBox="0 0 488 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M488 261.8C488 403.3 391.1 504 248 504 110.8 504 0 393.2 0 256S110.8 8 248 8c66.8 0 123 24.5 166.3 64.9l-67.5 64.9C258.5 52.6 94.3 116.6 94.3 256c0 86.5 69.1 156.6 153.7 156.6 98.2 0 135-70.4 140.8-106.9H248v-85.3h236.1c2.3 12.7 3.9 24.9 3.9 41.4z"
fill="currentColor"
style={{}}
style={Object {}}
/>
</svg>
</div>
@@ -93,7 +93,7 @@ exports[`SocialAuthProviders should match social auth provider with iconClass sn
`;
exports[`SocialAuthProviders should match social auth provider with iconImage snapshot 1`] = `
[
Array [
<button
className="btn-social btn-oa2-apple-id mr-3"
data-provider-url="/auth/login/apple-id/?auth_entry=login&next=/dashboard"

View File

@@ -21,7 +21,7 @@ exports[`ThirdPartyAuthAlert should match login page third party auth alert mess
`;
exports[`ThirdPartyAuthAlert should match register page third party auth alert message snapshot 1`] = `
[
Array [
<div
className="fade alert-content alert-success mt-n2 mb-5 alert show"
id="tpa-alert"

View File

@@ -5,23 +5,23 @@ exports[`Zendesk Help should match login page third party auth alert message sna
cookies={true}
defer={true}
webWidget={
{
"answerBot": {
"avatar": {
"name": {
Object {
"answerBot": Object {
"avatar": Object {
"name": Object {
"*": "edX Support",
},
"url": undefined,
},
"contactOnlyAfterQuery": true,
"suppress": false,
"title": {
"title": Object {
"*": "edX Support",
},
},
"chat": {
"departments": {
"enabled": [
"chat": Object {
"departments": Object {
"enabled": Array [
"account settings",
"billing and payments",
"certificates",
@@ -33,17 +33,17 @@ exports[`Zendesk Help should match login page third party auth alert message sna
},
"suppress": false,
},
"contactForm": {
"contactForm": Object {
"attachments": true,
"selectTicketForm": {
"selectTicketForm": Object {
"*": "Please choose your request type:",
},
"ticketForms": [
{
"fields": [
{
"ticketForms": Array [
Object {
"fields": Array [
Object {
"id": "description",
"prefill": {
"prefill": Object {
"*": "",
},
},
@@ -53,10 +53,10 @@ exports[`Zendesk Help should match login page third party auth alert message sna
},
],
},
"contactOptions": {
"contactOptions": Object {
"enabled": false,
},
"helpCenter": {
"helpCenter": Object {
"originalArticleButton": true,
},
}

View File

@@ -11,7 +11,6 @@ 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,
@@ -36,7 +35,6 @@ 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

@@ -37,4 +37,3 @@ export const VALID_EMAIL_REGEX = '(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+
// 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 REDIRECT = 'redirect';
export const APP_NAME = 'authn_mfe';

View File

@@ -1,6 +1,5 @@
import { combineReducers } from 'redux';
import { reducer as cohesionReducer, storeName as cohesionStoreName } from '../cohesion/data/reducers';
import {
reducer as commonComponentsReducer,
storeName as commonComponentsStoreName,
@@ -32,7 +31,6 @@ const createRootReducer = () => combineReducers({
[commonComponentsStoreName]: commonComponentsReducer,
[forgotPasswordStoreName]: forgotPasswordReducer,
[resetPasswordStoreName]: resetPasswordReducer,
[cohesionStoreName]: cohesionReducer,
[authnProgressiveProfilingStoreName]: authnProgressiveProfilingReducers,
});
export default createRootReducer;

View File

@@ -1,37 +0,0 @@
/* 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,11 +11,3 @@ 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

@@ -81,9 +81,3 @@ export const isHostAvailableInQueryParams = () => {
const queryParams = getAllPossibleQueryParams();
return 'host' in queryParams;
};
export const redirectWithDelay = (redirectUrl) => {
setTimeout(() => {
window.location.href = redirectUrl;
}, 1000);
};

View File

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

View File

@@ -2,6 +2,7 @@ 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,
@@ -24,10 +25,6 @@ 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;
@@ -44,8 +41,8 @@ const ForgotPasswordPage = (props) => {
const navigate = useNavigate();
useEffect(() => {
trackForgotPasswordPageEvent();
trackForgotPasswordPageViewed();
sendPageEvent('login_and_registration', 'reset');
sendTrackEvent('edx.bi.password_reset_form.viewed', { category: 'user-engagement' });
}, []);
useEffect(() => {

View File

@@ -12,7 +12,6 @@ import { ErrorPage } from '@edx/frontend-platform/react';
import configuration from './config';
import messages from './i18n';
import MainApp from './MainApp';
import './cohesion/cohesion.scss';
subscribe(APP_READY, () => {
ReactDOM.render(

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { Alert, Hyperlink } from '@openedx/paragon';
import { Alert } from '@openedx/paragon';
import { CheckCircle, Error } from '@openedx/paragon/icons';
import PropTypes from 'prop-types';
@@ -36,17 +36,19 @@ const AccountActivationMessage = ({ messageType }) => {
break;
}
case ACCOUNT_ACTIVATION_MESSAGE.ERROR: {
const supportEmail = (
<Hyperlink isInline destination={`mailto:${getConfig().INFO_EMAIL}`}>{getConfig().INFO_EMAIL}</Hyperlink>
const supportLink = (
<Alert.Link href={getConfig().ACTIVATION_EMAIL_SUPPORT_LINK}>
{formatMessage(messages['account.activation.support.link'])}
</Alert.Link>
);
heading = formatMessage(messages[`account.${activationOrConfirmation}.error.message.title`]);
activationMessage = (
<FormattedMessage
id="account.activation.error.message"
defaultMessage="Something went wrong, please contact {supportEmail} to resolve this issue."
defaultMessage="Something went wrong, please {supportLink} to resolve this issue."
description="Account activation error message"
values={{ supportEmail }}
values={{ supportLink }}
/>
);
break;

View File

@@ -1,5 +1,4 @@
import React, { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
@@ -11,23 +10,19 @@ import PropTypes from 'prop-types';
import { Link, useNavigate } from 'react-router-dom';
import messages from './messages';
import trackCohesionEvent from '../cohesion/trackers';
import { DEFAULT_REDIRECT_URL, RESET_PAGE } from '../data/constants';
import { updatePathWithQueryParams } from '../data/utils';
import { redirectWithDelay } from '../data/utils/dataUtils';
import useMobileResponsive from '../data/utils/useMobileResponsive';
const ChangePasswordPrompt = ({ variant, redirectUrl }) => {
const isMobileView = useMobileResponsive();
const [redirectToResetPasswordPage, setRedirectToResetPasswordPage] = useState(false);
const cohesionEventData = useSelector(state => state.cohesion.eventData);
const handlers = {
handleToggleOff: () => {
if (variant === 'block') {
setRedirectToResetPasswordPage(true);
} else {
trackCohesionEvent(cohesionEventData);
redirectWithDelay(redirectUrl || getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL));
window.location.href = redirectUrl || getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
}
},
};

View File

@@ -1,7 +1,8 @@
import React, { useEffect, useMemo, useState } from 'react';
import { connect, useDispatch } from 'react-redux';
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,
@@ -20,10 +21,6 @@ import {
import { INVALID_FORM, TPA_AUTHENTICATION_FAILURE } from './data/constants';
import LoginFailureMessage from './LoginFailure';
import messages from './messages';
import {
ELEMENT_NAME, ELEMENT_TEXT, ELEMENT_TYPES, PAGE_TYPES,
} from '../cohesion/constants';
import { setCohesionEventStates } from '../cohesion/data/actions';
import {
FormGroup,
InstitutionLogistration,
@@ -35,7 +32,9 @@ import { getThirdPartyAuthContext } from '../common-components/data/actions';
import { thirdPartyAuthContextSelector } from '../common-components/data/selectors';
import EnterpriseSSO from '../common-components/EnterpriseSSO';
import ThirdPartyAuth from '../common-components/ThirdPartyAuth';
import { DEFAULT_STATE, PENDING_STATE, RESET_PAGE } from '../data/constants';
import {
DEFAULT_STATE, PENDING_STATE, RESET_PAGE,
} from '../data/constants';
import {
getActivationStatus,
getAllPossibleQueryParams,
@@ -43,11 +42,7 @@ import {
getTpaProvider,
updatePathWithQueryParams,
} from '../data/utils';
import { removeCookie } from '../data/utils/cookies';
import ResetPasswordSuccess from '../reset-password/ResetPasswordSuccess';
import {
trackForgotPasswordLinkClick, trackLoginPageViewed, trackLoginSuccess,
} from '../tracking/trackers/login';
const LoginPage = (props) => {
const {
@@ -74,7 +69,6 @@ const LoginPage = (props) => {
getTPADataFromBackend,
} = props;
const { formatMessage } = useIntl();
const dispatch = useDispatch();
const activationMsgType = getActivationStatus();
const queryParams = useMemo(() => getAllPossibleQueryParams(), []);
@@ -84,17 +78,9 @@ const LoginPage = (props) => {
const tpaHint = getTpaHint();
useEffect(() => {
trackLoginPageViewed();
sendPageEvent('login_and_registration', 'login');
}, []);
useEffect(() => {
if (loginResult.success) {
trackLoginSuccess();
// Remove this cookie that was set to capture marketingEmailsOptIn for the onboarding component
removeCookie('ssoPipelineRedirectionDone');
}
}, [loginResult]);
useEffect(() => {
const payload = { ...queryParams };
if (tpaHint) {
@@ -154,15 +140,6 @@ const LoginPage = (props) => {
const handleSubmit = (event) => {
event.preventDefault();
const eventData = {
pageType: PAGE_TYPES.SIGN_IN,
elementType: ELEMENT_TYPES.BUTTON,
webElementText: ELEMENT_TEXT.SIGN_IN,
webElementName: ELEMENT_NAME.SIGN_IN,
};
dispatch(setCohesionEventStates(eventData));
if (showResetPasswordSuccessBanner) {
props.dismissPasswordResetBanner();
}
@@ -193,6 +170,9 @@ 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);
@@ -228,7 +208,6 @@ const LoginPage = (props) => {
success={loginResult.success}
redirectUrl={loginResult.redirectUrl}
finishAuthUrl={finishAuthUrl}
currentProvider={currentProvider}
/>
<div className="mw-xs mt-3 mb-2">
<LoginFailureMessage

View File

@@ -95,6 +95,11 @@ const messages = defineMessages({
defaultMessage: 'Your account could not be activated',
description: 'Account Activation error message title',
},
'account.activation.support.link': {
id: 'account.activation.support.link',
defaultMessage: 'contact support',
description: 'Link text used in account activation error message to go to learner help center',
},
// Email Confirmation Strings
'account.confirmation.success.message.title': {
id: 'account.confirmation.success.message.title',

View File

@@ -58,7 +58,7 @@ describe('AccountActivationMessage', () => {
);
const expectedMessage = 'Your account could not be activated'
+ 'Something went wrong, please contact to resolve this issue.';
+ 'Something went wrong, please contact support to resolve this issue.';
expect(screen.getByText(
'',
@@ -121,7 +121,7 @@ describe('EmailConfirmationMessage', () => {
</IntlProvider>,
);
const expectedMessage = 'Your email could not be confirmed'
+ 'Something went wrong, please contact to resolve this issue.';
+ 'Something went wrong, please contact support to resolve this issue.';
expect(screen.getByText(
'',
{ selector: '#account-activation-message' },

View File

@@ -1,30 +1,18 @@
import React from 'react';
import { Provider } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import {
fireEvent, render, screen, waitFor,
fireEvent, render, screen,
} from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import { MemoryRouter } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import mockTagular from '../../cohesion/utils';
import { RESET_PAGE } from '../../data/constants';
import ChangePasswordPrompt from '../ChangePasswordPrompt';
const IntlChangePasswordPrompt = injectIntl(ChangePasswordPrompt);
const mockedNavigator = jest.fn();
const mockStore = configureStore();
mockTagular();
const eventData = {
pageType: 'test-page',
elementType: 'test-element-type',
webElementText: 'test-element-text',
webElementName: 'test-element-name',
};
jest.mock('react-router-dom', () => ({
...(jest.requireActual('react-router-dom')),
@@ -33,14 +21,8 @@ jest.mock('react-router-dom', () => ({
describe('ChangePasswordPromptTests', () => {
let props = {};
let store = {};
const initialState = {
cohesion: { eventData: {} },
};
beforeAll(() => {
store = mockStore(initialState);
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
@@ -49,56 +31,38 @@ describe('ChangePasswordPromptTests', () => {
});
});
it('[nudge modal] should redirect to next url when user clicks close button', async () => {
it('[nudge modal] should redirect to next url when user clicks close button', () => {
const dashboardUrl = getConfig().BASE_URL.concat('/dashboard');
props = {
variant: 'nudge',
redirectUrl: dashboardUrl,
};
store = mockStore({
...initialState,
cohesion: {
eventData,
},
});
delete window.location;
window.location = { href: getConfig().BASE_URL };
render(
<IntlProvider locale="en">
<Provider store={store}>
<MemoryRouter>
<IntlChangePasswordPrompt {...props} />
</MemoryRouter>
</Provider>
<MemoryRouter>
<IntlChangePasswordPrompt {...props} />
</MemoryRouter>
</IntlProvider>,
);
fireEvent.click(screen.getByText('Close'));
await waitFor(() => {
expect(window.location.href).toBe(dashboardUrl);
}, { timeout: 1100 });
expect(window.location.href).toBe(dashboardUrl);
});
it('[block modal] should redirect to reset password page when user clicks outside modal', async () => {
props = {
variant: 'block',
};
store = mockStore({
...initialState,
cohesion: {
eventData,
},
});
render(
<IntlProvider locale="en">
<Provider store={store}>
<MemoryRouter>
<IntlChangePasswordPrompt {...props} />
</MemoryRouter>
</Provider>
<MemoryRouter>
<IntlChangePasswordPrompt {...props} />
</MemoryRouter>
</IntlProvider>,
);

View File

@@ -1,12 +1,10 @@
import React from 'react';
import { Provider } from 'react-redux';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import {
render, screen,
} from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import {
ACCOUNT_LOCKED_OUT,
@@ -27,27 +25,13 @@ import LoginFailureMessage from '../LoginFailure';
jest.mock('@edx/frontend-platform/auth', () => ({
getAuthService: jest.fn(),
}));
const mockStore = configureStore();
const IntlLoginFailureMessage = injectIntl(LoginFailureMessage);
const eventData = {
pageType: 'test-page',
elementType: 'test-element-type',
webElementText: 'test-element-text',
webElementName: 'test-element-name',
};
describe('LoginFailureMessage', () => {
let props = {};
let store = {};
const initialState = {
cohesion: { eventData: {} },
};
beforeAll(() => {
store = mockStore(initialState);
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
@@ -314,19 +298,11 @@ describe('LoginFailureMessage', () => {
errorCount: 0,
};
store = mockStore({
...initialState,
cohesion: {
eventData,
},
});
render(
<IntlProvider locale="en">
<Provider store={store}>
<MemoryRouter>
<IntlLoginFailureMessage {...props} />
</MemoryRouter>
</Provider>
<MemoryRouter>
<IntlLoginFailureMessage {...props} />
</MemoryRouter>
</IntlProvider>,
);
@@ -347,20 +323,12 @@ describe('LoginFailureMessage', () => {
errorCode: REQUIRE_PASSWORD_CHANGE,
errorCount: 0,
};
store = mockStore({
...initialState,
cohesion: {
eventData,
},
});
render(
<IntlProvider locale="en">
<Provider store={store}>
<MemoryRouter>
<IntlLoginFailureMessage {...props} />
</MemoryRouter>
</Provider>
<MemoryRouter>
<IntlLoginFailureMessage {...props} />
</MemoryRouter>
</IntlProvider>,
);

View File

@@ -11,10 +11,7 @@ import { act } from 'react-dom/test-utils';
import { MemoryRouter } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import mockTagular from '../../cohesion/utils';
import {
APP_NAME, COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE,
} from '../../data/constants';
import { 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';
@@ -26,7 +23,6 @@ jest.mock('@edx/frontend-platform/analytics', () => ({
jest.mock('@edx/frontend-platform/auth', () => ({
getAuthService: jest.fn(),
}));
mockTagular();
const IntlLoginPage = injectIntl(LoginPage);
const mockStore = configureStore();
@@ -60,7 +56,6 @@ describe('LoginPage', () => {
register: {
validationApiRateLimited: false,
},
cohesion: { eventData: {} },
};
const secondaryProviders = {
@@ -515,7 +510,7 @@ describe('LoginPage', () => {
// ******** test redirection ********
it('should redirect to url returned by login endpoint after successful authentication', async () => {
it('should redirect to url returned by login endpoint after successful authentication', () => {
const dashboardURL = 'https://test.com/testing-dashboard/';
store = mockStore({
...initialState,
@@ -531,12 +526,10 @@ describe('LoginPage', () => {
delete window.location;
window.location = { href: getConfig().BASE_URL };
render(reduxWrapper(<IntlLoginPage {...props} />));
await waitFor(() => {
expect(window.location.href).toBe(dashboardURL);
}, { timeout: 1100 });
expect(window.location.href).toBe(dashboardURL);
});
it('should redirect to finishAuthUrl upon successful login via SSO', async () => {
it('should redirect to finishAuthUrl upon successful login via SSO', () => {
const authCompleteUrl = '/auth/complete/google-oauth2/';
store = mockStore({
...initialState,
@@ -560,12 +553,10 @@ describe('LoginPage', () => {
window.location = { href: getConfig().BASE_URL };
render(reduxWrapper(<IntlLoginPage {...props} />));
await waitFor(() => {
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + authCompleteUrl);
}, { timeout: 1100 });
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + authCompleteUrl);
});
it('should redirect to social auth provider url on SSO button click', async () => {
it('should redirect to social auth provider url on SSO button click', () => {
store = mockStore({
...initialState,
commonComponents: {
@@ -586,12 +577,10 @@ describe('LoginPage', () => {
'',
{ selector: '#oa2-apple-id' },
));
await waitFor(() => {
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + ssoProvider.loginUrl);
}, { timeout: 1100 });
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + ssoProvider.loginUrl);
});
it('should redirect to finishAuthUrl upon successful authentication via SSO', async () => {
it('should redirect to finishAuthUrl upon successful authentication via SSO', () => {
const finishAuthUrl = '/auth/complete/google-oauth2/';
store = mockStore({
...initialState,
@@ -612,9 +601,7 @@ describe('LoginPage', () => {
window.location = { href: getConfig().BASE_URL };
render(reduxWrapper(<IntlLoginPage {...props} />));
await waitFor(() => {
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + finishAuthUrl);
}, { timeout: 1100 });
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + finishAuthUrl);
});
// ******** test hinted third party auth ********
@@ -764,7 +751,7 @@ describe('LoginPage', () => {
it('should send page event when login page is rendered', () => {
render(reduxWrapper(<IntlLoginPage {...props} />));
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'login', { app_name: APP_NAME });
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'login');
});
it('tests that form is in invalid state when it is submitted', () => {
@@ -797,7 +784,7 @@ describe('LoginPage', () => {
{ selector: '#forgot-password' },
));
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.password-reset_form.toggled', { category: 'user-engagement', app_name: APP_NAME });
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.password-reset_form.toggled', { category: 'user-engagement' });
});
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 { APP_NAME, LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
import { 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', app_name: APP_NAME });
sendTrackEvent('edx.bi.institution_login_form.toggled', { category: 'user-engagement' });
if (typeof e === 'string') {
sendPageEvent('login_and_registration', e === '/login' ? 'login' : 'register', { app_name: APP_NAME });
sendPageEvent('login_and_registration', e === '/login' ? 'login' : 'register');
} else {
sendPageEvent('login_and_registration', e.target.dataset.eventName, { app_name: APP_NAME });
sendPageEvent('login_and_registration', e.target.dataset.eventName);
}
setInstitutionLogin(!institutionLogin);
@@ -70,7 +70,7 @@ const Logistration = (props) => {
if (tabKey === currentTab) {
return;
}
sendTrackEvent(`edx.bi.${tabKey.replace('/', '')}_form.toggled`, { category: 'user-engagement', app_name: APP_NAME });
sendTrackEvent(`edx.bi.${tabKey.replace('/', '')}_form.toggled`, { category: 'user-engagement' });
props.clearThirdPartyAuthContextErrorMessage();
if (tabKey === LOGIN_PAGE) {
props.backupRegistrationForm();

View File

@@ -11,21 +11,16 @@ 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);
@@ -69,7 +64,6 @@ describe('Logistration', () => {
usernameSuggestions: [],
validationApiRateLimited: false,
},
cohesion: { eventData: {} },
commonComponents: {
thirdPartyAuthContext: {
providers: [],
@@ -90,7 +84,6 @@ describe('Logistration', () => {
})),
}));
useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
configure({
loggingService: { logError: jest.fn() },
config: {
@@ -231,8 +224,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', app_name: APP_NAME });
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'institution_login', { app_name: APP_NAME });
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.institution_login_form.toggled', { category: 'user-engagement' });
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'institution_login');
mergeConfig({
DISABLE_ENTERPRISE_LOGIN: '',

View File

@@ -1,29 +0,0 @@
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import { render } from '@testing-library/react';
import MainAppSlot from './index';
jest.mock('@openedx/frontend-plugin-framework', () => ({
PluginSlot: jest.fn(() => null),
}));
describe('MainAppSlot', () => {
it('renders without crashing', () => {
render(<MainAppSlot />);
});
it('renders a PluginSlot component', () => {
render(<MainAppSlot />);
expect(PluginSlot).toHaveBeenCalled();
});
it('passes the correct id prop to PluginSlot', () => {
render(<MainAppSlot />);
expect(PluginSlot).toHaveBeenCalledWith({ id: 'main_app_slot' }, {});
});
it('does not render any children', () => {
const { container } = render(<MainAppSlot />);
expect(container.firstChild).toBeNull();
});
});

View File

@@ -1,41 +0,0 @@
# Main App Slot
### Slot ID: `main_app_slot`
## Description
This slot is used for adding content at the root level.
## Example
The following `env.config.jsx` will render a component at the MFE root level.
![Screenshot of Content added after the Main App Slot](./images/main_app_slot.png)
```js
import {
DIRECT_PLUGIN,
PLUGIN_OPERATIONS,
} from "@openedx/frontend-plugin-framework";
import { ExampleComponent } from "@openedx/frontend-plugin-example";
const config = {
pluginSlots: {
main_app_slot: {
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: "example-component",
type: DIRECT_PLUGIN,
priority: 60,
RenderWidget: ExampleComponent,
},
},
],
},
},
};
export default config;
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 269 KiB

View File

@@ -1,7 +0,0 @@
import { PluginSlot } from '@openedx/frontend-plugin-framework';
const MainAppSlot = () => (
<PluginSlot id="main_app_slot" />
);
export default MainAppSlot;

View File

@@ -1,3 +0,0 @@
# `frontend-app-authn` Plugin Slots
- [`main_app_slot`](./MainAppSlot/)

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 } from '@edx/frontend-platform/analytics';
import { identifyAuthenticatedUser, sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import {
AxiosJwtAuthService,
configure as configureAuth,
@@ -39,13 +39,6 @@ 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();
@@ -105,13 +98,14 @@ const ProgressiveProfiling = (props) => {
useEffect(() => {
if (authenticatedUser?.userId) {
identifyAuthenticatedUser(authenticatedUser.userId);
trackProgressiveProfilingPageViewed();
sendPageEvent('login_and_registration', 'welcome');
}
}, [authenticatedUser]);
useEffect(() => {
if (!enablePostRegistrationRecommendations) {
trackDisablePostRegistrationRecommendations(
sendTrackEvent(
'edx.bi.user.recommendations.not.enabled',
{ functionalCookiesConsent, page: 'authn_recommendations' },
);
return;
@@ -155,23 +149,29 @@ const ProgressiveProfiling = (props) => {
});
}
props.saveUserProfile(authenticatedUser.username, snakeCaseObject(payload));
const eventProperties = {
isGenderSelected: !!values.gender,
isYearOfBirthSelected: !!values.year_of_birth,
isLevelOfEducationSelected: !!values.level_of_education,
isWorkExperienceSelected: !!values.work_experience,
host: queryParams?.host || '',
};
trackProgressiveProfilingSubmitClick(eventProperties);
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 handleSkip = (e) => {
e.preventDefault();
window.history.replaceState(location.state, null, '');
setShowModal(true);
trackProgressiveProfilingSkipLinkClick({
host: queryParams?.host || '',
});
sendTrackEvent(
'edx.bi.welcome.page.skip.link.clicked',
{
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={() => (trackProgressiveProfilingSupportLinkCLick())}
onClick={() => (sendTrackEvent('edx.bi.welcome.page.support.link.clicked'))}
>
{formatMessage(messages['optional.fields.information.link'])}
</Hyperlink>

View File

@@ -6,14 +6,12 @@ import { identifyAuthenticatedUser, sendTrackEvent } from '@edx/frontend-platfor
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { configure, injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import {
fireEvent, render, screen, waitFor,
fireEvent, render, screen,
} from '@testing-library/react';
import { MemoryRouter, mockNavigate, useLocation } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import mockTagular from '../../cohesion/utils';
import {
APP_NAME,
AUTHN_PROGRESSIVE_PROFILING,
COMPLETE_STATE, DEFAULT_REDIRECT_URL,
EMBEDDED,
@@ -26,7 +24,6 @@ import ProgressiveProfiling from '../ProgressiveProfiling';
const IntlProgressiveProfilingPage = injectIntl(ProgressiveProfiling);
const mockStore = configureStore();
mockTagular();
jest.mock('@edx/frontend-platform/analytics', () => ({
sendPageEvent: jest.fn(),
@@ -57,13 +54,6 @@ jest.mock('react-router-dom', () => {
};
});
const eventData = {
pageType: 'test-page',
elementType: 'test-element-type',
webElementText: 'test-element-text',
webElementName: 'test-element-name',
};
describe('ProgressiveProfilingTests', () => {
let store = {};
@@ -153,9 +143,8 @@ 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', payload);
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.skip.link.clicked', { host: '' });
});
// ******** test event functionality ********
@@ -176,7 +165,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', { app_name: APP_NAME });
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.support.link.clicked');
});
it('should set empty host property value for non-embedded experience', () => {
@@ -186,7 +175,6 @@ describe('ProgressiveProfilingTests', () => {
isLevelOfEducationSelected: false,
isWorkExperienceSelected: false,
host: '',
app_name: APP_NAME,
};
delete window.location;
window.location = { href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING) };
@@ -261,9 +249,6 @@ describe('ProgressiveProfilingTests', () => {
...initialState.welcomePage,
success: true,
},
cohesion: {
eventData,
},
});
const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
const nextButton = container.querySelector('button.btn-brand');
@@ -290,18 +275,13 @@ describe('ProgressiveProfilingTests', () => {
...initialState.welcomePage,
success: true,
},
cohesion: {
eventData,
},
});
const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
const nextButton = container.querySelector('button.btn-brand');
expect(nextButton.textContent).toEqual('Submit');
await waitFor(() => {
expect(window.location.href).toEqual(redirectUrl);
}, { timeout: 1100 });
expect(window.location.href).toEqual(redirectUrl);
});
});
@@ -336,7 +316,7 @@ describe('ProgressiveProfilingTests', () => {
const skipLinkButton = screen.getByText('Skip for now');
fireEvent.click(skipLinkButton);
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.skip.link.clicked', { host, app_name: APP_NAME });
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.skip.link.clicked', { host });
});
it('should show spinner while fetching the optional fields', () => {
@@ -369,7 +349,6 @@ describe('ProgressiveProfilingTests', () => {
isLevelOfEducationSelected: false,
isWorkExperienceSelected: false,
host: 'http://example.com',
app_name: APP_NAME,
};
delete window.location;
window.location = {
@@ -416,7 +395,7 @@ describe('ProgressiveProfilingTests', () => {
expect(window.location.href).toBe(DASHBOARD_URL);
});
it('should redirect to provided redirect url', async () => {
it('should redirect to provided redirect url', () => {
const redirectUrl = 'https://redirect-test.com';
delete window.location;
window.location = {
@@ -438,17 +417,12 @@ describe('ProgressiveProfilingTests', () => {
...initialState.welcomePage,
success: true,
},
cohesion: {
eventData,
},
});
render(reduxWrapper(<IntlProgressiveProfilingPage />));
const submitButton = screen.getByText('Submit');
fireEvent.click(submitButton);
await waitFor(() => {
expect(window.location.href).toBe(redirectUrl);
}, { timeout: 1100 });
expect(window.location.href).toBe(redirectUrl);
});
});
});

View File

@@ -15,7 +15,7 @@ const generateProductKey = (product) => (
export const getProductMapping = (recommendedProducts) => recommendedProducts.map((product) => ({
product_key: generateProductKey(product),
product_line: product.cardType,
product_source: product?.productSource?.name,
product_source: product.productSource.name,
}));
export const trackRecommendationClick = (product, position, userId) => {
@@ -25,7 +25,7 @@ export const trackRecommendationClick = (product, position, userId) => {
recommendation_type: product.recommendationType,
product_key: generateProductKey(product),
product_line: product.cardType,
product_source: product?.productSource?.name,
product_source: product.productSource.name,
user_id: userId,
});

View File

@@ -11,7 +11,7 @@ export const INVALID_NAME_REGEX = /https?:\/\/(?:[-\w.]|(?:%[\da-fA-F]{2}))*/g;
const validateName = (value, formatMessage) => {
let fieldError = '';
if (!value || (value && !value.trim())) {
if (!value.trim()) {
fieldError = formatMessage(messages['empty.name.field.error']);
} else if (URL_REGEX.test(value) || HTML_REGEX.test(value) || INVALID_NAME_REGEX.test(value)) {
fieldError = formatMessage(messages['name.validation.message']);

View File

@@ -101,7 +101,7 @@ const UsernameField = (props) => {
};
const suggestedUsernames = () => (
<div className={className}>
<div className={className} role="listbox">
<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,7 +112,9 @@ 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>
@@ -123,7 +125,7 @@ const UsernameField = (props) => {
);
if (usernameSuggestions.length > 0 && errorMessage && value === ' ') {
className = 'username-suggestions__error';
className = 'username-suggestions';
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 === ' ') {
@@ -134,14 +136,15 @@ const UsernameField = (props) => {
suggestedUsernameDiv = suggestedUsernames();
}
return (
<FormGroup
{...props}
handleChange={handleOnChange}
handleFocus={handleOnFocus}
handleBlur={handleOnBlur}
>
<div className="username__form-group-wrapper">
{suggestedUsernameDiv}
</FormGroup>
<FormGroup
{...props}
handleChange={handleOnChange}
handleFocus={handleOnFocus}
handleBlur={handleOnBlur}
/>
</div>
);
};

View File

@@ -4,6 +4,7 @@ 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';
@@ -11,19 +12,12 @@ import PropTypes from 'prop-types';
import { Helmet } from 'react-helmet';
import Skeleton from 'react-loading-skeleton';
import {
InstitutionLogistration,
PasswordField,
RedirectLogistration,
ThirdPartyAuthAlert,
} from '../common-components';
import ConfigurableRegistrationForm from './components/ConfigurableRegistrationForm';
import RegistrationFailure from './components/RegistrationFailure';
import {
backupRegistrationFormBegin,
clearRegistrationBackendError,
registerNewUser,
setAutoGeneratedUsernameExperimentData,
setEmailSuggestionInStore,
setUserPipelineDataLoaded,
} from './data/actions';
@@ -31,8 +25,6 @@ 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,
@@ -40,20 +32,21 @@ import {
import messages from './messages';
import { EmailField, NameField, UsernameField } from './RegistrationFields';
import {
ELEMENT_NAME, ELEMENT_TEXT, ELEMENT_TYPES, PAGE_TYPES,
} from '../cohesion/constants';
import { setCohesionEventStates } from '../cohesion/data/actions';
InstitutionLogistration,
PasswordField,
RedirectLogistration,
ThirdPartyAuthAlert,
} from '../common-components';
import { getThirdPartyAuthContext as getRegistrationDataFromBackend } from '../common-components/data/actions';
import EnterpriseSSO from '../common-components/EnterpriseSSO';
import ThirdPartyAuth from '../common-components/ThirdPartyAuth';
import {
APP_NAME, COMPLETE_STATE, PENDING_STATE,
REGISTER_PAGE,
COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE,
} from '../data/constants';
import {
getAllPossibleQueryParams, getTpaHint, getTpaProvider, isHostAvailableInQueryParams, removeCookie, setCookie,
getAllPossibleQueryParams, getTpaHint, getTpaProvider, isHostAvailableInQueryParams, setCookie,
} from '../data/utils';
import { trackRegistrationPageViewed, trackRegistrationSuccess } from '../tracking/trackers/register';
/**
* Main Registration Page component
*/
@@ -75,7 +68,6 @@ 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);
@@ -111,12 +103,6 @@ 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
*/
@@ -142,7 +128,7 @@ const RegistrationPage = (props) => {
useEffect(() => {
if (!formStartTime) {
trackRegistrationPageViewed();
sendPageEvent('login_and_registration', 'register');
const payload = { ...queryParams, is_register_page: true };
if (tpaHint) {
payload.tpa_hint = tpaHint;
@@ -163,10 +149,8 @@ const RegistrationPage = (props) => {
formFields: { ...formFields },
errors: { ...errors },
}));
dispatch(setAutoGeneratedUsernameExperimentData(autoGeneratedUsernameExpVariation));
}
}, [shouldBackupState, configurableFormFields, // eslint-disable-line react-hooks/exhaustive-deps
formFields, errors, dispatch, backedUpFormData]);
}, [shouldBackupState, configurableFormFields, formFields, errors, dispatch, backedUpFormData]);
useEffect(() => {
if (backendValidations) {
@@ -187,15 +171,10 @@ const RegistrationPage = (props) => {
useEffect(() => {
if (registrationResult.success) {
// This event is used by GTM
trackRegistrationSuccess();
sendTrackEvent('edx.bi.user.account.registered.client', {});
// 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');
// Remove this cookie that was set to capture marketingEmailsOptIn for the onboarding component
removeCookie('ssoPipelineRedirectionDone');
}
}, [registrationResult]);
@@ -231,13 +210,13 @@ const RegistrationPage = (props) => {
const registerUser = () => {
const totalRegistrationTime = (Date.now() - formStartTime) / 1000;
let payload = { ...formFields, app_name: APP_NAME };
let payload = { ...formFields };
if (currentProvider) {
delete payload.password;
payload.social_auth_provider = currentProvider;
}
if (hideUsernameField) {
if (flags.autoGeneratedUsernameEnabled) {
delete payload.username;
}
@@ -272,14 +251,6 @@ const RegistrationPage = (props) => {
const handleSubmit = (e) => {
e.preventDefault();
const eventData = {
pageType: PAGE_TYPES.ACCOUNT_CREATION,
elementType: ELEMENT_TYPES.BUTTON,
webElementText: ELEMENT_TEXT.CREATE_ACCOUNT,
webElementName: ELEMENT_NAME.CREATE_ACCOUNT,
};
dispatch(setCohesionEventStates(eventData));
registerUser();
};
@@ -314,111 +285,107 @@ const RegistrationPage = (props) => {
redirectToProgressiveProfilingPage={
getConfig().ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN && !!Object.keys(optionalFields.fields).length
}
currentProvider={currentProvider}
/>
{(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 },
{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'])}
/>
<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'])}
/>
{!flags.autoGeneratedUsernameEnabled && (
<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'])}
/>
)}
>
<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()}
{!currentProvider && (
<PasswordField
name="password"
value={formFields.password}
handleChange={handleOnChange}
handleErrorChange={handleErrorChange}
errorMessage={errors.name}
helpText={[formatMessage(messages['help.text.name'])]}
floatingLabel={formatMessage(messages['registration.fullname.label'])}
errorMessage={errors.password}
floatingLabel={formatMessage(messages['registration.password.label'])}
/>
<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'])}
)}
<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}
/>
<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>
)}
)}
</Form>
</div>
)}
</>
);
};

View File

@@ -6,7 +6,7 @@ import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics'
import {
configure, getLocale, injectIntl, IntlProvider,
} from '@edx/frontend-platform/i18n';
import { fireEvent, render, waitFor } from '@testing-library/react';
import { fireEvent, render } from '@testing-library/react';
import { mockNavigate, BrowserRouter as Router } from 'react-router-dom';
import configureStore from 'redux-mock-store';
@@ -17,13 +17,9 @@ import {
setUserPipelineDataLoaded,
} from './data/actions';
import { INTERNAL_SERVER_ERROR } from './data/constants';
import mockTagular from '../cohesion/utils';
import { NOT_INITIALIZED } from './data/optimizelyExperiment/helper';
import useAutoGeneratedUsernameExperimentVariation
from './data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
import RegistrationPage from './RegistrationPage';
import {
APP_NAME, AUTHN_PROGRESSIVE_PROFILING, COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE,
AUTHN_PROGRESSIVE_PROFILING, COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE,
} from '../data/constants';
jest.mock('@edx/frontend-platform/analytics', () => ({
@@ -34,11 +30,9 @@ 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();
mockTagular();
jest.mock('react-router-dom', () => {
const mockNavigation = jest.fn();
@@ -108,7 +102,6 @@ describe('RegistrationPage', () => {
usernameSuggestions: [],
},
cohesion: { eventData: {} },
commonComponents: {
thirdPartyAuthApiStatus: null,
thirdPartyAuthContext,
@@ -135,7 +128,6 @@ describe('RegistrationPage', () => {
institutionLogin: false,
};
window.location = { search: '' };
useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
});
afterEach(() => {
@@ -191,9 +183,8 @@ describe('RegistrationPage', () => {
password: 'password1',
country: 'Pakistan',
honor_code: true,
total_registration_time: 0,
totalRegistrationTime: 0,
next: '/course/demo-course-url',
app_name: APP_NAME,
};
store.dispatch = jest.fn(store.dispatch);
@@ -215,8 +206,7 @@ describe('RegistrationPage', () => {
country: 'Pakistan',
honor_code: true,
social_auth_provider: 'Apple',
total_registration_time: 0,
app_name: APP_NAME,
totalRegistrationTime: 0,
};
store = mockStore({
@@ -249,7 +239,7 @@ describe('RegistrationPage', () => {
password: 'password1',
country: 'Ukraine',
honor_code: true,
total_registration_time: 0,
totalRegistrationTime: 0,
};
store.dispatch = jest.fn(store.dispatch);
@@ -274,7 +264,7 @@ describe('RegistrationPage', () => {
password: 'password1',
country: 'Ukraine',
honor_code: true,
total_registration_time: 0,
totalRegistrationTime: 0,
};
store.dispatch = jest.fn(store.dispatch);
@@ -300,9 +290,8 @@ describe('RegistrationPage', () => {
password: 'password1',
country: 'Pakistan',
honor_code: true,
total_registration_time: 0,
totalRegistrationTime: 0,
marketing_emails_opt_in: true,
app_name: APP_NAME,
};
store.dispatch = jest.fn(store.dispatch);
@@ -328,8 +317,7 @@ describe('RegistrationPage', () => {
password: 'password1',
country: 'Pakistan',
honor_code: true,
total_registration_time: 0,
app_name: APP_NAME,
totalRegistrationTime: 0,
};
store.dispatch = jest.fn(store.dispatch);
@@ -508,7 +496,7 @@ describe('RegistrationPage', () => {
expect(document.cookie).toMatch(`${getConfig().USER_RETENTION_COOKIE_NAME}=true`);
});
it('should redirect to url returned in registration result after successful account creation', async () => {
it('should redirect to url returned in registration result after successful account creation', () => {
const dashboardURL = 'https://test.com/testing-dashboard/';
store = mockStore({
...initialState,
@@ -523,12 +511,10 @@ describe('RegistrationPage', () => {
delete window.location;
window.location = { href: getConfig().BASE_URL };
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
await waitFor(() => {
expect(window.location.href).toBe(dashboardURL);
}, { timeout: 1100 });
expect(window.location.href).toBe(dashboardURL);
});
it('should redirect to dashboard if features flags are configured but no optional fields are configured', async () => {
it('should redirect to dashboard if features flags are configured but no optional fields are configured', () => {
mergeConfig({
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN: true,
});
@@ -552,9 +538,7 @@ describe('RegistrationPage', () => {
delete window.location;
window.location = { href: getConfig().BASE_URL };
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
await waitFor(() => {
expect(window.location.href).toBe(dashboardUrl);
}, { timeout: 1100 });
expect(window.location.href).toBe(dashboardUrl);
});
it('should redirect to progressive profiling page if optional fields are configured', () => {
@@ -608,7 +592,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', { app_name: APP_NAME });
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'register');
});
it('should send track event when user has successfully registered', () => {
@@ -626,7 +610,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', { app_name: APP_NAME });
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.user.account.registered.client', {});
});
it('should populate form with pipeline user details', () => {
@@ -898,8 +882,7 @@ describe('RegistrationPage', () => {
email: 'john.doe@example.com',
country: 'PK',
social_auth_provider: 'Apple',
total_registration_time: 0,
app_name: APP_NAME,
totalRegistrationTime: 0,
}));
});
});

View File

@@ -1,16 +1,10 @@
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 {
ELEMENT_NAME, ELEMENT_TEXT, ELEMENT_TYPES, PAGE_TYPES,
} from '../../cohesion/constants';
import trackCohesionEvent from '../../cohesion/trackers';
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';
@@ -38,16 +32,12 @@ 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
confused and unable to create an account. So we added the United States entry in the dropdown list.
*/
const countryList = useMemo(() => (
getCountryList(getLocale()).concat([{ code: 'US', name: 'United States' }]).filter(country => country.code !== 'RU')
), []);
const countryList = useMemo(() => getCountryList(getLocale()).concat([{ code: 'US', name: 'United States' }]), []);
let showTermsOfServiceAndHonorCode = false;
let showCountryField = false;
@@ -60,8 +50,6 @@ 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.
*/
@@ -102,25 +90,6 @@ const ConfigurableRegistrationForm = (props) => {
setFieldErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
}
}
// setting marketingEmailsOptIn state for SSO authentication flow for register API call
if (name === 'marketingEmailsOptIn') {
if (!value) {
const cohesionEventData = {
pageType: PAGE_TYPES.ACCOUNT_CREATION,
elementType: ELEMENT_TYPES.BUTTON,
webElementText: ELEMENT_TEXT.OPT_IN_TEXT,
webElementName: ELEMENT_NAME.OPT_OUT,
};
trackCohesionEvent(cohesionEventData);
}
dispatch(backupRegistrationFormBegin({
...backedUpFormData,
configurableFormFields: {
...backedUpFormData.configurableFormFields,
[name]: value,
},
}));
}
setFormFields(prevState => ({ ...prevState, [name]: value }));
};

View File

@@ -9,12 +9,8 @@ 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';
@@ -26,7 +22,6 @@ 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);
@@ -99,7 +94,6 @@ describe('ConfigurableRegistrationForm', () => {
registrationFormData,
usernameSuggestions: [],
},
cohesion: { eventData: {} },
commonComponents: {
thirdPartyAuthApiStatus: null,
thirdPartyAuthContext,
@@ -127,7 +121,6 @@ describe('ConfigurableRegistrationForm', () => {
};
window.location = { search: '' };
getLocale.mockImplementationOnce(() => ('en-us'));
useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
});
afterEach(() => {
@@ -252,7 +245,7 @@ describe('ConfigurableRegistrationForm', () => {
country: 'Pakistan',
honor_code: true,
profession: 'Engineer',
total_registration_time: 0,
totalRegistrationTime: 0,
};
store.dispatch = jest.fn(store.dispatch);
@@ -267,7 +260,7 @@ describe('ConfigurableRegistrationForm', () => {
fireEvent.click(submitButton);
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK', app_name: APP_NAME }));
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
});
it('should show error messages for required fields on empty form submission', () => {
@@ -363,7 +356,7 @@ describe('ConfigurableRegistrationForm', () => {
password: 'password1',
country: 'Ukraine',
honor_code: true,
total_registration_time: 0,
totalRegistrationTime: 0,
};
store = mockStore({

View File

@@ -12,9 +12,6 @@ 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';
@@ -26,7 +23,6 @@ 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);
@@ -99,7 +95,6 @@ describe('RegistrationFailure', () => {
registrationFormData,
usernameSuggestions: [],
},
cohesion: { eventData: {} },
commonComponents: {
thirdPartyAuthApiStatus: null,
thirdPartyAuthContext,
@@ -126,7 +121,6 @@ describe('RegistrationFailure', () => {
institutionLogin: false,
};
window.location = { search: '' };
useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
});
afterEach(() => {

View File

@@ -5,17 +5,13 @@ import { getConfig, mergeConfig } from '@edx/frontend-platform';
import {
configure, getLocale, injectIntl, IntlProvider,
} from '@edx/frontend-platform/i18n';
import { fireEvent, render, waitFor } from '@testing-library/react';
import { fireEvent, render } from '@testing-library/react';
import { BrowserRouter as Router } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import mockTagular from '../../../cohesion/utils';
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', () => ({
@@ -26,8 +22,6 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(),
}));
jest.mock('../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
mockTagular();
const IntlRegistrationPage = injectIntl(RegistrationPage);
const mockStore = configureStore();
@@ -100,7 +94,6 @@ describe('ThirdPartyAuth', () => {
registrationFormData,
usernameSuggestions: [],
},
cohesion: { eventData: {} },
commonComponents: {
thirdPartyAuthApiStatus: null,
thirdPartyAuthContext,
@@ -127,7 +120,6 @@ describe('ThirdPartyAuth', () => {
institutionLogin: false,
};
window.location = { search: '' };
useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
});
afterEach(() => {
@@ -342,7 +334,7 @@ describe('ThirdPartyAuth', () => {
expect(headingElement).toBeTruthy();
});
it('should redirect to social auth provider url on SSO button click', async () => {
it('should redirect to social auth provider url on SSO button click', () => {
const registerUrl = '/auth/login/apple-id/?auth_entry=register&next=/dashboard';
store = mockStore({
...initialState,
@@ -368,12 +360,10 @@ describe('ThirdPartyAuth', () => {
const ssoButton = container.querySelector('button#oa2-apple-id');
fireEvent.click(ssoButton);
await waitFor(() => {
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + registerUrl);
}, { timeout: 1100 });
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + registerUrl);
});
it('should redirect to finishAuthUrl upon successful registration via SSO', async () => {
it('should redirect to finishAuthUrl upon successful registration via SSO', () => {
const authCompleteUrl = '/auth/complete/google-oauth2/';
store = mockStore({
...initialState,
@@ -396,9 +386,7 @@ describe('ThirdPartyAuth', () => {
window.location = { href: getConfig().BASE_URL };
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
await waitFor(() => {
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + authCompleteUrl);
}, { timeout: 1100 });
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + authCompleteUrl);
});
// ******** test alert messages ********

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,9 +83,3 @@ 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

@@ -1,30 +0,0 @@
/**
* 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

@@ -1,53 +0,0 @@
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,7 +3,6 @@ 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,
@@ -40,7 +39,6 @@ export const defaultState = {
usernameSuggestions: [],
validationApiRateLimited: false,
shouldBackupState: false,
autoGeneratedUsernameExperimentVariation: '',
};
const reducer = (state = defaultState, action = {}) => {
@@ -57,12 +55,6 @@ 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,11 +1,14 @@
import { camelCaseObject } from '@edx/frontend-platform';
import { logError, logInfo } from '@edx/frontend-platform/logging';
import { call, put, takeEvery } from 'redux-saga/effects';
import {
call, put, race, take, takeEvery,
} from 'redux-saga/effects';
import {
fetchRealtimeValidationsBegin,
fetchRealtimeValidationsFailure,
fetchRealtimeValidationsSuccess,
REGISTER_CLEAR_USERNAME_SUGGESTIONS,
REGISTER_FORM_VALIDATIONS,
REGISTER_NEW_USER,
registerNewUserBegin,
@@ -41,9 +44,15 @@ export function* handleNewUserRegistration(action) {
export function* fetchRealtimeValidations(action) {
try {
yield put(fetchRealtimeValidationsBegin());
const { fieldValidations } = yield call(getFieldsValidations, action.payload.formPayload);
yield put(fetchRealtimeValidationsSuccess(camelCaseObject(fieldValidations)));
const { response } = yield race({
response: call(getFieldsValidations, action.payload.formPayload),
cancel: take(REGISTER_CLEAR_USERNAME_SUGGESTIONS),
});
if (response) {
yield put(fetchRealtimeValidationsSuccess(camelCaseObject(response.fieldValidations)));
}
} catch (e) {
if (e.response && e.response.status === 403) {
yield put(fetchRealtimeValidationsFailure());

View File

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

View File

@@ -1,77 +0,0 @@
import { isFormValid } from '../utils';
describe('Payload validation', () => {
let formatMessage;
let configurableFormFields;
let fieldDescriptions;
beforeEach(() => {
formatMessage = jest.fn(msg => msg);
configurableFormFields = {
confirm_email: true,
};
fieldDescriptions = {};
});
test('validates name field correctly', () => {
const payload = { name: ' ' };
const errors = {};
const { isValid, fieldErrors } = isFormValid(
payload,
errors,
configurableFormFields,
fieldDescriptions,
formatMessage);
expect(fieldErrors.name).toBeDefined();
expect(isValid).toBe(false);
});
test('validates email field correctly', () => {
const payload = { email: 'invalid-email' };
const errors = {};
const { isValid, fieldErrors } = isFormValid(
payload, errors, configurableFormFields, fieldDescriptions, formatMessage);
expect(fieldErrors.email).toBeDefined();
expect(isValid).toBe(false);
});
test('validates username field correctly', () => {
const payload = { username: 'invalid username' };
const errors = {};
const { isValid, fieldErrors } = isFormValid(
payload, errors, configurableFormFields, fieldDescriptions, formatMessage);
expect(fieldErrors.username).toBeDefined();
expect(isValid).toBe(false);
});
test('validates password field correctly', () => {
const payload = { password: 'short' };
const errors = {};
const { isValid, fieldErrors } = isFormValid(
payload, errors, configurableFormFields, fieldDescriptions, formatMessage);
expect(fieldErrors.password).toBeDefined();
expect(isValid).toBe(false);
});
test('validates multiple fields correctly', () => {
const payload = {
name: 'InvalidName!',
email: 'invalid-email',
username: 'invalid username',
password: 'short',
};
const errors = {};
const { isValid, fieldErrors } = isFormValid(
payload, errors, configurableFormFields, fieldDescriptions, formatMessage);
expect(fieldErrors.name).toBeDefined();
expect(fieldErrors.email).toBeDefined();
expect(fieldErrors.username).toBeDefined();
expect(fieldErrors.password).toBeDefined();
expect(isValid).toBe(false);
});
});

View File

@@ -59,7 +59,6 @@ export const isFormValid = (
isValid = false;
}
emailSuggestion = suggestion;
if (fieldErrors.email) { isValid = false; }
break;
}
case 'username':
@@ -126,8 +125,8 @@ export const prepareRegistrationPayload = (
delete payload.marketingEmailsOptIn;
}
payload.totalRegistrationTime = totalRegistrationTime;
payload = snakeCaseObject(payload);
payload.totalRegistrationTime = totalRegistrationTime;
// add query params to the payload
payload = { ...payload, ...queryParams };

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, SUCCESS, TOKEN_STATE,
FORM_SUBMISSION_ERROR, PASSWORD_RESET_ERROR, PASSWORD_VALIDATION_ERROR, TOKEN_STATE,
} from './data/constants';
import { resetPasswordResultSelector } from './data/selectors';
import { validatePassword } from './data/service';
@@ -30,7 +30,6 @@ 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();
@@ -43,15 +42,6 @@ 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);
@@ -149,7 +139,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,11 +19,6 @@ 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,10 +65,15 @@
margin-right: 0.25rem;
}
.username-suggestions {
.username__form-group-wrapper {
position: relative;
margin-top: -2.5rem;
margin-left: 15px;
}
.username-suggestions {
position: absolute;
inset: 0;
padding-left: 15px;
z-index: 100;
}
.username-suggestions__close__button {
@@ -76,13 +81,6 @@
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

@@ -1,22 +0,0 @@
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

@@ -1,29 +0,0 @@
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

@@ -1,37 +0,0 @@
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

@@ -1,22 +0,0 @@
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

@@ -1,14 +0,0 @@
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

@@ -1,37 +0,0 @@
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

@@ -1,37 +0,0 @@
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

@@ -1,37 +0,0 @@
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

@@ -1,36 +0,0 @@
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

@@ -1,26 +0,0 @@
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',
);
});
});

View File

@@ -1,14 +0,0 @@
const path = require('path');
const { createConfig } = require('@openedx/frontend-build');
const config = createConfig('webpack-dev');
config.resolve.modules = [
path.resolve(__dirname, './src'),
'node_modules',
];
config.module.rules[0].exclude = /node_modules\/(?!(fastest-levenshtein|@edx))/;
module.exports = config;

View File

@@ -1,14 +1,7 @@
const path = require('path');
const { createConfig } = require('@openedx/frontend-build');
const config = createConfig('webpack-prod');
config.resolve.modules = [
path.resolve(__dirname, './src'),
'node_modules',
];
config.module.rules[0].exclude = /node_modules\/(?!(fastest-levenshtein|@edx))/;
module.exports = config;