Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
a8f90cfc1b build(deps): bump actions/setup-node from 4 to 6
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-20 03:19:23 +00:00
12 changed files with 229 additions and 401 deletions

View File

@@ -16,7 +16,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Nodejs
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'

199
package-lock.json generated
View File

@@ -16,7 +16,6 @@
"@fortawesome/free-brands-svg-icons": "6.7.2",
"@fortawesome/free-solid-svg-icons": "6.7.2",
"@fortawesome/react-fontawesome": "0.2.6",
"@openedx/frontend-plugin-framework": "^1.7.0",
"@openedx/paragon": "^23.4.2",
"@optimizely/react-sdk": "^2.9.1",
"@redux-devtools/extension": "3.3.0",
@@ -41,7 +40,7 @@
"redux": "4.2.1",
"redux-logger": "3.0.6",
"redux-mock-store": "1.5.5",
"redux-saga": "1.4.2",
"redux-saga": "1.3.0",
"redux-thunk": "2.4.2",
"regenerator-runtime": "0.14.1",
"reselect": "5.1.1",
@@ -2538,9 +2537,9 @@
}
},
"node_modules/@edx/frontend-platform": {
"version": "8.5.2",
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-8.5.2.tgz",
"integrity": "sha512-YlxNWs8NW/I7F03k/jH6grWIuY/GJrspq7fqWm5K0ocvNEf+B8XKcaLUof+jVUuCItK93SoVRDZewwejnjty5w==",
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-8.5.1.tgz",
"integrity": "sha512-8u3EdO0o7xX4vqorjOx3k2wbs2bu3DXlIA3bnD+Y56vSB5QYw6k5GzYqo9pPaTMGeq9TuLRvPLE/QFFlc3xvPg==",
"license": "AGPL-3.0",
"dependencies": {
"@cospired/i18n-iso-languages": "4.2.0",
@@ -7061,87 +7060,10 @@
"node": ">=10"
}
},
"node_modules/@openedx/frontend-plugin-framework": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@openedx/frontend-plugin-framework/-/frontend-plugin-framework-1.7.0.tgz",
"integrity": "sha512-8tGkuHvtzhbqb9dU4sXUtR0K44+Hjh1uGR6DvhZAt9wSKQC1v4RBk34ef8DFzQhoNQa/Jtn6BJuta4Un6MmHmw==",
"license": "AGPL-3.0",
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"classnames": "^2.3.2",
"core-js": "3.37.1",
"react-redux": "8.1.1",
"redux": "4.2.1"
},
"peerDependencies": {
"@edx/frontend-platform": "^7.0.0 || ^8.0.0",
"@openedx/paragon": "^21.0.0 || ^22.0.0 || ^23.0.0",
"prop-types": "^15.8.0",
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0",
"react-error-boundary": "^4.0.11"
}
},
"node_modules/@openedx/frontend-plugin-framework/node_modules/core-js": {
"version": "3.37.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.37.1.tgz",
"integrity": "sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw==",
"hasInstallScript": true,
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/@openedx/frontend-plugin-framework/node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"license": "MIT"
},
"node_modules/@openedx/frontend-plugin-framework/node_modules/react-redux": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.1.tgz",
"integrity": "sha512-5W0QaKtEhj+3bC0Nj0NkqkhIv8gLADH/2kYFMTHxCVqQILiWzLv6MaLuV5wJU3BQEdHKzTfcvPN0WMS6SC1oyA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.1",
"@types/hoist-non-react-statics": "^3.3.1",
"@types/use-sync-external-store": "^0.0.3",
"hoist-non-react-statics": "^3.3.2",
"react-is": "^18.0.0",
"use-sync-external-store": "^1.0.0"
},
"peerDependencies": {
"@types/react": "^16.8 || ^17.0 || ^18.0",
"@types/react-dom": "^16.8 || ^17.0 || ^18.0",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0",
"react-native": ">=0.59",
"redux": "^4 || ^5.0.0-beta.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
},
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
},
"redux": {
"optional": true
}
}
},
"node_modules/@openedx/paragon": {
"version": "23.15.2",
"resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-23.15.2.tgz",
"integrity": "sha512-qHiEwcBBmvlXQ13j+bE3VaL9UEpb37ZLpR3+iryiWb+GZPva9VhZkvTMHORklD3jcFCoNt2TkKWZ906sxPJ0zw==",
"version": "23.14.9",
"resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-23.14.9.tgz",
"integrity": "sha512-IwM6UZJE6lKmGCyGV52oO+40m0y2kFAoOdzyFdw3ud+Wc0Ro3bcflPnuNuq2Wq5a3ceJ9RLu9g+uKx0Yv9yvrw==",
"license": "Apache-2.0",
"peer": true,
"workspaces": [
@@ -7853,17 +7775,17 @@
}
},
"node_modules/@redux-saga/core": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.4.2.tgz",
"integrity": "sha512-nIMLGKo6jV6Wc1sqtVQs1iqbB3Kq20udB/u9XEaZQisT6YZ0NRB8+4L6WqD/E+YziYutd27NJbG8EWUPkb7c6Q==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.3.0.tgz",
"integrity": "sha512-L+i+qIGuyWn7CIg7k1MteHGfttKPmxwZR5E7OsGikCL2LzYA0RERlaUY00Y3P3ZV2EYgrsYlBrGs6cJP5OKKqA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.28.4",
"@redux-saga/deferred": "^1.3.1",
"@redux-saga/delay-p": "^1.3.1",
"@redux-saga/is": "^1.2.1",
"@redux-saga/symbols": "^1.2.1",
"@redux-saga/types": "^1.3.1",
"@babel/runtime": "^7.6.3",
"@redux-saga/deferred": "^1.2.1",
"@redux-saga/delay-p": "^1.2.1",
"@redux-saga/is": "^1.1.3",
"@redux-saga/symbols": "^1.1.3",
"@redux-saga/types": "^1.2.1",
"typescript-tuple": "^2.2.1"
},
"funding": {
@@ -7872,40 +7794,40 @@
}
},
"node_modules/@redux-saga/deferred": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@redux-saga/deferred/-/deferred-1.3.1.tgz",
"integrity": "sha512-0YZ4DUivWojXBqLB/TmuRRpDDz7tyq1I0AuDV7qi01XlLhM5m51W7+xYtIckH5U2cMlv9eAuicsfRAi1XHpXIg==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@redux-saga/deferred/-/deferred-1.2.1.tgz",
"integrity": "sha512-cmin3IuuzMdfQjA0lG4B+jX+9HdTgHZZ+6u3jRAOwGUxy77GSlTi4Qp2d6PM1PUoTmQUR5aijlA39scWWPF31g==",
"license": "MIT"
},
"node_modules/@redux-saga/delay-p": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@redux-saga/delay-p/-/delay-p-1.3.1.tgz",
"integrity": "sha512-597I7L5MXbD/1i3EmcaOOjL/5suxJD7p5tnbV1PiWnE28c2cYiIHqmSMK2s7us2/UrhOL2KTNBiD0qBg6KnImg==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@redux-saga/delay-p/-/delay-p-1.2.1.tgz",
"integrity": "sha512-MdiDxZdvb1m+Y0s4/hgdcAXntpUytr9g0hpcOO1XFVyyzkrDu3SKPgBFOtHn7lhu7n24ZKIAT1qtKyQjHqRd+w==",
"license": "MIT",
"dependencies": {
"@redux-saga/symbols": "^1.2.1"
"@redux-saga/symbols": "^1.1.3"
}
},
"node_modules/@redux-saga/is": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@redux-saga/is/-/is-1.2.1.tgz",
"integrity": "sha512-x3aWtX3GmQfEvn8dh0ovPbsXgK9JjpiR24wKztpGbZP8JZUWWvUgKrvnWZ/T/4iphOBftyVc9VrIwhAnsM+OFA==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@redux-saga/is/-/is-1.1.3.tgz",
"integrity": "sha512-naXrkETG1jLRfVfhOx/ZdLj0EyAzHYbgJWkXbB3qFliPcHKiWbv/ULQryOAEKyjrhiclmr6AMdgsXFyx7/yE6Q==",
"license": "MIT",
"dependencies": {
"@redux-saga/symbols": "^1.2.1",
"@redux-saga/types": "^1.3.1"
"@redux-saga/symbols": "^1.1.3",
"@redux-saga/types": "^1.2.1"
}
},
"node_modules/@redux-saga/symbols": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@redux-saga/symbols/-/symbols-1.2.1.tgz",
"integrity": "sha512-3dh+uDvpBXi7EUp/eO+N7eFM4xKaU4yuGBXc50KnZGzIrR/vlvkTFQsX13zsY8PB6sCFYAgROfPSRUj8331QSA==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@redux-saga/symbols/-/symbols-1.1.3.tgz",
"integrity": "sha512-hCx6ZvU4QAEUojETnX8EVg4ubNLBFl1Lps4j2tX7o45x/2qg37m3c6v+kSp8xjDJY+2tJw4QB3j8o8dsl1FDXg==",
"license": "MIT"
},
"node_modules/@redux-saga/types": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@redux-saga/types/-/types-1.3.1.tgz",
"integrity": "sha512-YRCrJdhQLobGIQ8Cj1sta3nn6DrZDTSUnrIYhS2e5V590BmfVDleKoAquclAiKSBKWJwmuXTb+b4BL6rSHnahw==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@redux-saga/types/-/types-1.2.1.tgz",
"integrity": "sha512-1dgmkh+3so0+LlBWRhGA33ua4MYr7tUOj+a9Si28vUi0IUFNbff1T3sgpeDJI/LaC75bBYnQ0A3wXjn0OrRNBA==",
"license": "MIT"
},
"node_modules/@remix-run/router": {
@@ -8764,13 +8686,12 @@
"license": "MIT"
},
"node_modules/@types/react": {
"version": "18.3.26",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz",
"integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==",
"version": "19.1.13",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz",
"integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
}
},
@@ -8866,12 +8787,6 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/@types/use-sync-external-store": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz",
"integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==",
"license": "MIT"
},
"node_modules/@types/warning": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz",
@@ -23963,19 +23878,6 @@
"node": ">= 12"
}
},
"node_modules/react-error-boundary": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.1.2.tgz",
"integrity": "sha512-GQDxZ5Jd+Aq/qUxbCm1UtzmL/s++V7zKgE8yMktJiCQXCCFZnMZh9ng+6/Ne6PjNSXH0L9CjeOEREfRnq6Duag==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.12.5"
},
"peerDependencies": {
"react": ">=16.13.1"
}
},
"node_modules/react-error-overlay": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.1.0.tgz",
@@ -24137,6 +24039,16 @@
"tslib": "2"
}
},
"node_modules/react-intl/node_modules/@types/react": {
"version": "18.3.24",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.24.tgz",
"integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==",
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@@ -24557,12 +24469,12 @@
}
},
"node_modules/redux-saga": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.4.2.tgz",
"integrity": "sha512-QLIn/q+7MX/B+MkGJ/K6R3//60eJ4QNy65eqPsJrfGezbxdh1Jx+37VRKE2K4PsJnNET5JufJtgWdT30WBa+6w==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.3.0.tgz",
"integrity": "sha512-J9RvCeAZXSTAibFY0kGw6Iy4EdyDNW7k6Q+liwX+bsck7QVsU78zz8vpBRweEfANxnnlG/xGGeOvf6r8UXzNJQ==",
"license": "MIT",
"dependencies": {
"@redux-saga/core": "^1.4.2"
"@redux-saga/core": "^1.3.0"
}
},
"node_modules/redux-thunk": {
@@ -27536,15 +27448,6 @@
}
}
},
"node_modules/use-sync-external-store": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/util": {
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",

View File

@@ -36,7 +36,6 @@
"@fortawesome/free-brands-svg-icons": "6.7.2",
"@fortawesome/free-solid-svg-icons": "6.7.2",
"@fortawesome/react-fontawesome": "0.2.6",
"@openedx/frontend-plugin-framework": "^1.7.0",
"@openedx/paragon": "^23.4.2",
"@optimizely/react-sdk": "^2.9.1",
"@redux-devtools/extension": "3.3.0",
@@ -61,7 +60,7 @@
"redux": "4.2.1",
"redux-logger": "3.0.6",
"redux-mock-store": "1.5.5",
"redux-saga": "1.4.2",
"redux-saga": "1.3.0",
"redux-thunk": "2.4.2",
"regenerator-runtime": "0.14.1",
"reselect": "5.1.1",

View File

@@ -1,17 +1,26 @@
import {
useCallback, useEffect, useMemo, useState,
} from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useEffect, useMemo, useState } from 'react';
import { connect } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Form, StatefulButton } from '@openedx/paragon';
import {
Form, StatefulButton,
} from '@openedx/paragon';
import PropTypes from 'prop-types';
import { Helmet } from 'react-helmet';
import Skeleton from 'react-loading-skeleton';
import { Link } from 'react-router-dom';
import AccountActivationMessage from './AccountActivationMessage';
import {
backupLoginFormBegin,
dismissPasswordResetBanner,
loginRequest,
} from './data/actions';
import { INVALID_FORM, TPA_AUTHENTICATION_FAILURE } from './data/constants';
import LoginFailureMessage from './LoginFailure';
import messages from './messages';
import {
FormGroup,
InstitutionLogistration,
@@ -19,12 +28,13 @@ import {
RedirectLogistration,
ThirdPartyAuthAlert,
} from '../common-components';
import AccountActivationMessage from './AccountActivationMessage';
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 { PENDING_STATE, RESET_PAGE } from '../data/constants';
import {
DEFAULT_STATE, PENDING_STATE, RESET_PAGE,
} from '../data/constants';
import {
getActivationStatus,
getAllPossibleQueryParams,
@@ -33,57 +43,37 @@ import {
updatePathWithQueryParams,
} from '../data/utils';
import ResetPasswordSuccess from '../reset-password/ResetPasswordSuccess';
import { backupLoginFormBegin, dismissPasswordResetBanner, loginRequest } from './data/actions';
import { INVALID_FORM, TPA_AUTHENTICATION_FAILURE } from './data/constants';
import LoginFailureMessage from './LoginFailure';
import messages from './messages';
const LoginPage = ({
institutionLogin,
handleInstitutionLogin,
}) => {
const dispatch = useDispatch();
const backupFormState = useCallback((data) => dispatch(backupLoginFormBegin(data)), [dispatch]);
const getTPADataFromBackend = useCallback(() => dispatch(getThirdPartyAuthContext()), [dispatch]);
const LoginPage = (props) => {
const {
backedUpFormData,
loginErrorCode,
loginErrorContext,
loginResult,
shouldBackupState,
thirdPartyAuthContext: {
providers,
currentProvider,
secondaryProviders,
finishAuthUrl,
platformName,
errorMessage: thirdPartyErrorMessage,
},
thirdPartyAuthApiStatus,
institutionLogin,
showResetPasswordSuccessBanner,
submitState,
thirdPartyAuthContext,
thirdPartyAuthApiStatus,
} = useSelector((state) => ({
backedUpFormData: state.login.loginFormData,
loginErrorCode: state.login.loginErrorCode,
loginErrorContext: state.login.loginErrorContext,
loginResult: state.login.loginResult,
shouldBackupState: state.login.shouldBackupState,
showResetPasswordSuccessBanner: state.login.showResetPasswordSuccessBanner,
submitState: state.login.submitState,
thirdPartyAuthContext: thirdPartyAuthContextSelector(state),
thirdPartyAuthApiStatus: state.commonComponents.thirdPartyAuthApiStatus,
}));
const {
providers,
currentProvider,
secondaryProviders,
finishAuthUrl,
platformName,
errorMessage: thirdPartyErrorMessage,
} = thirdPartyAuthContext;
// Actions
backupFormState,
handleInstitutionLogin,
getTPADataFromBackend,
} = props;
const { formatMessage } = useIntl();
const activationMsgType = getActivationStatus();
const queryParams = useMemo(() => getAllPossibleQueryParams(), []);
const [formFields, setFormFields] = useState({ ...backedUpFormData.formFields });
const [errorCode, setErrorCode] = useState({
type: '',
count: 0,
context: {},
});
const [errorCode, setErrorCode] = useState({ type: '', count: 0, context: {} });
const [errors, setErrors] = useState({ ...backedUpFormData.errors });
const tpaHint = getTpaHint();
@@ -97,7 +87,7 @@ const LoginPage = ({
payload.tpa_hint = tpaHint;
}
getTPADataFromBackend(payload);
}, [queryParams, tpaHint, getTPADataFromBackend]);
}, [getTPADataFromBackend, queryParams, tpaHint]);
/**
* Backup the login form in redux when login page is toggled.
*/
@@ -108,7 +98,7 @@ const LoginPage = ({
errors: { ...errors },
});
}
}, [backupFormState, shouldBackupState, formFields, errors]);
}, [shouldBackupState, formFields, errors, backupFormState]);
useEffect(() => {
if (loginErrorCode) {
@@ -133,10 +123,7 @@ const LoginPage = ({
}, [thirdPartyErrorMessage]);
const validateFormFields = (payload) => {
const {
emailOrUsername,
password,
} = payload;
const { emailOrUsername, password } = payload;
const fieldErrors = { ...errors };
if (emailOrUsername === '') {
@@ -154,18 +141,14 @@ const LoginPage = ({
const handleSubmit = (event) => {
event.preventDefault();
if (showResetPasswordSuccessBanner) {
dispatch(dismissPasswordResetBanner());
props.dismissPasswordResetBanner();
}
const formData = { ...formFields };
const validationErrors = validateFormFields(formData);
if (validationErrors.emailOrUsername || validationErrors.password) {
setErrors({ ...validationErrors });
setErrorCode(prevState => ({
type: INVALID_FORM,
count: prevState.count + 1,
context: {},
}));
setErrorCode(prevState => ({ type: INVALID_FORM, count: prevState.count + 1, context: {} }));
return;
}
@@ -175,35 +158,23 @@ const LoginPage = ({
password: formData.password,
...queryParams,
};
dispatch(loginRequest(payload));
props.loginRequest(payload);
};
const handleOnChange = (event) => {
const {
name,
value,
} = event.target;
setFormFields(prevState => ({
...prevState,
[name]: value,
}));
const { name, value } = event.target;
setFormFields(prevState => ({ ...prevState, [name]: value }));
};
const handleOnFocus = (event) => {
const { name } = event.target;
setErrors(prevErrors => ({
...prevErrors,
[name]: '',
}));
setErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
};
const trackForgotPasswordLinkClick = () => {
sendTrackEvent('edx.bi.password-reset_form.toggled', { category: 'user-engagement' });
};
const {
provider,
skipHintedLogin,
} = getTpaProvider(tpaHint, providers, secondaryProviders);
const { provider, skipHintedLogin } = getTpaProvider(tpaHint, providers, secondaryProviders);
if (tpaHint) {
if (thirdPartyAuthApiStatus === PENDING_STATE) {
@@ -310,9 +281,88 @@ const LoginPage = ({
);
};
const mapStateToProps = state => {
const loginPageState = state.login;
return {
backedUpFormData: loginPageState.loginFormData,
loginErrorCode: loginPageState.loginErrorCode,
loginErrorContext: loginPageState.loginErrorContext,
loginResult: loginPageState.loginResult,
shouldBackupState: loginPageState.shouldBackupState,
showResetPasswordSuccessBanner: loginPageState.showResetPasswordSuccessBanner,
submitState: loginPageState.submitState,
thirdPartyAuthContext: thirdPartyAuthContextSelector(state),
thirdPartyAuthApiStatus: state.commonComponents.thirdPartyAuthApiStatus,
};
};
LoginPage.propTypes = {
backedUpFormData: PropTypes.shape({
formFields: PropTypes.shape({}),
errors: PropTypes.shape({}),
}),
loginErrorCode: PropTypes.string,
loginErrorContext: PropTypes.shape({
email: PropTypes.string,
redirectUrl: PropTypes.string,
context: PropTypes.shape({}),
}),
loginResult: PropTypes.shape({
redirectUrl: PropTypes.string,
success: PropTypes.bool,
}),
shouldBackupState: PropTypes.bool,
showResetPasswordSuccessBanner: PropTypes.bool,
submitState: PropTypes.string,
thirdPartyAuthApiStatus: PropTypes.string,
institutionLogin: PropTypes.bool.isRequired,
thirdPartyAuthContext: PropTypes.shape({
currentProvider: PropTypes.string,
errorMessage: PropTypes.string,
platformName: PropTypes.string,
providers: PropTypes.arrayOf(PropTypes.shape({})),
secondaryProviders: PropTypes.arrayOf(PropTypes.shape({})),
finishAuthUrl: PropTypes.string,
}),
// Actions
backupFormState: PropTypes.func.isRequired,
dismissPasswordResetBanner: PropTypes.func.isRequired,
loginRequest: PropTypes.func.isRequired,
getTPADataFromBackend: PropTypes.func.isRequired,
handleInstitutionLogin: PropTypes.func.isRequired,
};
export default LoginPage;
LoginPage.defaultProps = {
backedUpFormData: {
formFields: {
emailOrUsername: '', password: '',
},
errors: {
emailOrUsername: '', password: '',
},
},
loginErrorCode: null,
loginErrorContext: {},
loginResult: {},
shouldBackupState: false,
showResetPasswordSuccessBanner: false,
submitState: DEFAULT_STATE,
thirdPartyAuthApiStatus: PENDING_STATE,
thirdPartyAuthContext: {
currentProvider: null,
errorMessage: null,
finishAuthUrl: null,
providers: [],
secondaryProviders: [],
},
};
export default connect(
mapStateToProps,
{
backupFormState: backupLoginFormBegin,
dismissPasswordResetBanner,
loginRequest,
getTPADataFromBackend: getThirdPartyAuthContext,
},
)(LoginPage);

View File

@@ -41,14 +41,6 @@ describe('LoginPage', () => {
const initialState = {
login: {
loginResult: { success: false, redirectUrl: '' },
loginFormData: {
formFields: {
emailOrUsername: '', password: '',
},
errors: {
emailOrUsername: '', password: '',
},
},
},
commonComponents: {
thirdPartyAuthApiStatus: null,

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { connect } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
@@ -24,20 +24,16 @@ import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
import {
getTpaHint, getTpaProvider, updatePathWithQueryParams,
} from '../data/utils';
import { LoginPage } from '../login';
import { backupLoginForm } from '../login/data/actions';
import LoginComponentSlot from '../plugin-slots/LoginComponentSlot';
import { RegistrationPage } from '../register';
import { backupRegistrationForm } from '../register/data/actions';
const Logistration = ({
selectedPage,
}) => {
const Logistration = (props) => {
const { selectedPage, tpaProviders } = props;
const tpaHint = getTpaHint();
const tpaProviders = useSelector(tpaProvidersSelector);
const dispatch = useDispatch();
const {
providers,
secondaryProviders,
providers, secondaryProviders,
} = tpaProviders;
const { formatMessage } = useIntl();
const [institutionLogin, setInstitutionLogin] = useState(false);
@@ -49,8 +45,7 @@ const Logistration = ({
useEffect(() => {
const authService = getAuthService();
if (authService) {
authService.getCsrfTokenService()
.getCsrfToken(getConfig().LMS_BASE_URL);
authService.getCsrfTokenService().getCsrfToken(getConfig().LMS_BASE_URL);
}
});
@@ -76,11 +71,11 @@ const Logistration = ({
return;
}
sendTrackEvent(`edx.bi.${tabKey.replace('/', '')}_form.toggled`, { category: 'user-engagement' });
dispatch(clearThirdPartyAuthContextErrorMessage());
props.clearThirdPartyAuthContextErrorMessage();
if (tabKey === LOGIN_PAGE) {
dispatch(backupRegistrationForm());
props.backupRegistrationForm();
} else if (tabKey === REGISTER_PAGE) {
dispatch(backupLoginForm());
props.backupLoginForm();
}
setKey(tabKey);
};
@@ -116,10 +111,7 @@ const Logistration = ({
{!institutionLogin && (
<h3 className="mb-4.5">{formatMessage(messages['logistration.sign.in'])}</h3>
)}
<LoginComponentSlot
institutionLogin={institutionLogin}
handleInstitutionLogin={handleInstitutionLogin}
/>
<LoginPage institutionLogin={institutionLogin} handleInstitutionLogin={handleInstitutionLogin} />
</div>
</>
)
@@ -132,16 +124,12 @@ const Logistration = ({
</Tabs>
)
: (!isValidTpaHint() && !hideRegistrationLink && (
<Tabs
defaultActiveKey={selectedPage}
id="controlled-tab"
onSelect={(tabKey) => handleOnSelect(tabKey, selectedPage)}
>
<Tabs defaultActiveKey={selectedPage} id="controlled-tab" onSelect={(tabKey) => handleOnSelect(tabKey, selectedPage)}>
<Tab title={formatMessage(messages['logistration.register'])} eventKey={REGISTER_PAGE} />
<Tab title={formatMessage(messages['logistration.sign.in'])} eventKey={LOGIN_PAGE} />
</Tabs>
))}
{key && (
{ key && (
<Navigate to={updatePathWithQueryParams(key)} replace />
)}
<div id="main-content" className="main-content">
@@ -151,12 +139,7 @@ const Logistration = ({
</h3>
)}
{selectedPage === LOGIN_PAGE
? (
<LoginComponentSlot
institutionLogin={institutionLogin}
handleInstitutionLogin={handleInstitutionLogin}
/>
)
? <LoginPage institutionLogin={institutionLogin} handleInstitutionLogin={handleInstitutionLogin} />
: (
<RegistrationPage
institutionLogin={institutionLogin}
@@ -173,10 +156,35 @@ const Logistration = ({
Logistration.propTypes = {
selectedPage: PropTypes.string,
backupLoginForm: PropTypes.func.isRequired,
backupRegistrationForm: PropTypes.func.isRequired,
clearThirdPartyAuthContextErrorMessage: PropTypes.func.isRequired,
tpaProviders: PropTypes.shape({
providers: PropTypes.arrayOf(PropTypes.shape({})),
secondaryProviders: PropTypes.arrayOf(PropTypes.shape({})),
}),
};
Logistration.defaultProps = {
tpaProviders: {
providers: [],
secondaryProviders: [],
},
};
Logistration.defaultProps = {
selectedPage: REGISTER_PAGE,
};
export default Logistration;
const mapStateToProps = state => ({
tpaProviders: tpaProvidersSelector(state),
});
export default connect(
mapStateToProps,
{
backupLoginForm,
backupRegistrationForm,
clearThirdPartyAuthContextErrorMessage,
},
)(Logistration);

View File

@@ -48,26 +48,16 @@ describe('Logistration', () => {
marketingEmailsOptIn: true,
},
formFields: {
name: '',
email: '',
username: '',
password: '',
name: '', email: '', username: '', password: '',
},
emailSuggestion: {
suggestion: '',
type: '',
suggestion: '', type: '',
},
errors: {
name: '',
email: '',
username: '',
password: '',
name: '', email: '', username: '', password: '',
},
},
registrationResult: {
success: false,
redirectUrl: '',
},
registrationResult: { success: false, redirectUrl: '' },
registrationError: {},
usernameSuggestions: [],
validationApiRateLimited: false,
@@ -79,18 +69,7 @@ describe('Logistration', () => {
},
},
login: {
loginResult: {
success: false,
redirectUrl: '',
},
loginFormData: {
formFields: {
emailOrUsername: '', password: '',
},
errors: {
emailOrUsername: '', password: '',
},
},
loginResult: { success: false, redirectUrl: '' },
},
};

View File

@@ -1,47 +0,0 @@
# Login Component Slot
### Slot ID: `org.openedx.frontend.authn.login_component.v1`
## Description
This slot is used to replace/modify/hide the login component.
## Example
### Default content
![Default Login Page](./default_component.png)
### With a prepended message
![Login Page with ](./component_with_prefix.png)
The following `env.config.jsx` will add a message before the login component.
```js
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
// Load environment variables from .env file
const config = {
...process.env,
pluginSlots: {
'org.openedx.frontend.authn.login_component.v1': {
keepDefault: true,
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'test_plugin',
type: DIRECT_PLUGIN,
priority: 1,
RenderWidget: () => (
<h2>You're logging into TEST Instance.</h2>
)
},
},
],
},
},
};
export default config;
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

View File

@@ -1,31 +0,0 @@
import React from 'react';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import PropTypes from 'prop-types';
import LoginPage from '../../login/LoginPage';
const LoginComponentSlot = ({
institutionLogin,
handleInstitutionLogin,
}) => (
<PluginSlot
id="org.openedx.frontend.authn.login_component.v1"
pluginProps={{
isInstitutionLogin: institutionLogin,
setInstitutionLogin: handleInstitutionLogin,
}}
>
<LoginPage
institutionLogin={institutionLogin}
handleInstitutionLogin={handleInstitutionLogin}
/>
</PluginSlot>
);
LoginComponentSlot.propTypes = {
institutionLogin: PropTypes.bool,
handleInstitutionLogin: PropTypes.func,
};
export default LoginComponentSlot;

View File

@@ -1,12 +1,10 @@
import React from 'react';
import { render } from '@testing-library/react';
import { renderHook } from '@testing-library/react';
import algoliasearchHelper from 'algoliasearch-helper';
import mockedRecommendedProducts from './mockedData';
import CreateAlgoliaSearchHelperMock from './test_utils/test_utils';
import isOneTrustFunctionalCookieEnabled from '../../../data/oneTrust';
import useAlgoliaRecommendations from '../hooks/useAlgoliaRecommendations';
import CreateAlgoliaSearchHelperMock from './test_utils/test_utils';
jest.mock('algoliasearch-helper');
@@ -19,23 +17,6 @@ jest.mock('../../../data/algolia', () => ({
jest.mock('../algoliaResultsParser', () => jest.fn((course) => course));
const renderHook = (hookCallback) => {
const result = {
current: null,
};
const Component = () => {
const val = hookCallback();
React.useEffect(() => {
result.current = val;
});
return null;
};
render(<Component />);
return { result };
};
describe('useAlgoliaRecommendations Tests', () => {
const MockSearchHelperWithData = new CreateAlgoliaSearchHelperMock(mockedRecommendedProducts);
const MockSearchHelperWithoutData = new CreateAlgoliaSearchHelperMock();
@@ -47,10 +28,8 @@ describe('useAlgoliaRecommendations Tests', () => {
() => useAlgoliaRecommendations('PK', 'Introductory'),
);
expect(result.current.recommendations)
.toEqual(mockedRecommendedProducts);
expect(result.current.isLoading)
.toBe(false);
expect(result.current.recommendations).toEqual(mockedRecommendedProducts);
expect(result.current.isLoading).toBe(false);
});
it('should not fetch recommendations if functional cookies are not set', async () => {
@@ -60,10 +39,8 @@ describe('useAlgoliaRecommendations Tests', () => {
() => useAlgoliaRecommendations('PK', 'Introductory'),
);
expect(result.current.recommendations)
.toEqual([]);
expect(result.current.isLoading)
.toBe(false);
expect(result.current.recommendations).toEqual([]);
expect(result.current.isLoading).toBe(false);
});
it('should return empty list if no recommendations returned from Algolia', async () => {
@@ -73,9 +50,7 @@ describe('useAlgoliaRecommendations Tests', () => {
() => useAlgoliaRecommendations('PK', 'Introductory'),
);
expect(result.current.recommendations)
.toEqual([]);
expect(result.current.isLoading)
.toBe(false);
expect(result.current.recommendations).toEqual([]);
expect(result.current.isLoading).toBe(false);
});
});