Skip to content

Commit fa40595

Browse files
committed
feat: Add plugin slot for login page
This change adds a plugin slot for the login page allowing it to be customised. Since there was a dependency conflict between frontend-plugin-framework and the react-hooks testing package, the react-hooks testing package has been removed and a replaced with a simple mechanism for testing hooks. Since this touched the Login Page those have also been refactored to move away from redux connect.
1 parent 97c7bd7 commit fa40595

File tree

11 files changed

+450
-256
lines changed

11 files changed

+450
-256
lines changed

package-lock.json

Lines changed: 199 additions & 80 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"@fortawesome/free-brands-svg-icons": "6.7.2",
3737
"@fortawesome/free-solid-svg-icons": "6.7.2",
3838
"@fortawesome/react-fontawesome": "0.2.6",
39+
"@openedx/frontend-plugin-framework": "^1.7.0",
3940
"@openedx/paragon": "^23.4.2",
4041
"@optimizely/react-sdk": "^2.9.1",
4142
"@redux-devtools/extension": "3.3.0",

src/login/LoginPage.jsx

Lines changed: 74 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,30 @@
1-
import { useEffect, useMemo, useState } from 'react';
2-
import { connect } from 'react-redux';
1+
import {
2+
useCallback, useEffect, useMemo, useState,
3+
} from 'react';
4+
import { useDispatch, useSelector } from 'react-redux';
35

46
import { getConfig } from '@edx/frontend-platform';
57
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
68
import { useIntl } from '@edx/frontend-platform/i18n';
7-
import {
8-
Form, StatefulButton,
9-
} from '@openedx/paragon';
9+
import { Form, StatefulButton } from '@openedx/paragon';
1010
import PropTypes from 'prop-types';
1111
import { Helmet } from 'react-helmet';
1212
import Skeleton from 'react-loading-skeleton';
1313
import { Link } from 'react-router-dom';
1414

15-
import AccountActivationMessage from './AccountActivationMessage';
16-
import {
17-
backupLoginFormBegin,
18-
dismissPasswordResetBanner,
19-
loginRequest,
20-
} from './data/actions';
21-
import { INVALID_FORM, TPA_AUTHENTICATION_FAILURE } from './data/constants';
22-
import LoginFailureMessage from './LoginFailure';
23-
import messages from './messages';
2415
import {
2516
FormGroup,
2617
InstitutionLogistration,
2718
PasswordField,
2819
RedirectLogistration,
2920
ThirdPartyAuthAlert,
3021
} from '../common-components';
22+
import AccountActivationMessage from './AccountActivationMessage';
3123
import { getThirdPartyAuthContext } from '../common-components/data/actions';
3224
import { thirdPartyAuthContextSelector } from '../common-components/data/selectors';
3325
import EnterpriseSSO from '../common-components/EnterpriseSSO';
3426
import ThirdPartyAuth from '../common-components/ThirdPartyAuth';
35-
import {
36-
DEFAULT_STATE, PENDING_STATE, RESET_PAGE,
37-
} from '../data/constants';
27+
import { PENDING_STATE, RESET_PAGE } from '../data/constants';
3828
import {
3929
getActivationStatus,
4030
getAllPossibleQueryParams,
@@ -43,37 +33,57 @@ import {
4333
updatePathWithQueryParams,
4434
} from '../data/utils';
4535
import ResetPasswordSuccess from '../reset-password/ResetPasswordSuccess';
36+
import { backupLoginFormBegin, dismissPasswordResetBanner, loginRequest } from './data/actions';
37+
import { INVALID_FORM, TPA_AUTHENTICATION_FAILURE } from './data/constants';
38+
import LoginFailureMessage from './LoginFailure';
39+
import messages from './messages';
4640

47-
const LoginPage = (props) => {
41+
const LoginPage = ({
42+
institutionLogin,
43+
handleInstitutionLogin,
44+
}) => {
45+
const dispatch = useDispatch();
46+
const backupFormState = useCallback((data) => dispatch(backupLoginFormBegin(data)), [dispatch]);
47+
const getTPADataFromBackend = useCallback(() => dispatch(getThirdPartyAuthContext()), [dispatch]);
4848
const {
4949
backedUpFormData,
5050
loginErrorCode,
5151
loginErrorContext,
5252
loginResult,
5353
shouldBackupState,
54-
thirdPartyAuthContext: {
55-
providers,
56-
currentProvider,
57-
secondaryProviders,
58-
finishAuthUrl,
59-
platformName,
60-
errorMessage: thirdPartyErrorMessage,
61-
},
62-
thirdPartyAuthApiStatus,
63-
institutionLogin,
6454
showResetPasswordSuccessBanner,
6555
submitState,
66-
// Actions
67-
backupFormState,
68-
handleInstitutionLogin,
69-
getTPADataFromBackend,
70-
} = props;
56+
thirdPartyAuthContext,
57+
thirdPartyAuthApiStatus,
58+
} = useSelector((state) => ({
59+
backedUpFormData: state.login.loginFormData,
60+
loginErrorCode: state.login.loginErrorCode,
61+
loginErrorContext: state.login.loginErrorContext,
62+
loginResult: state.login.loginResult,
63+
shouldBackupState: state.login.shouldBackupState,
64+
showResetPasswordSuccessBanner: state.login.showResetPasswordSuccessBanner,
65+
submitState: state.login.submitState,
66+
thirdPartyAuthContext: thirdPartyAuthContextSelector(state),
67+
thirdPartyAuthApiStatus: state.commonComponents.thirdPartyAuthApiStatus,
68+
}));
69+
const {
70+
providers,
71+
currentProvider,
72+
secondaryProviders,
73+
finishAuthUrl,
74+
platformName,
75+
errorMessage: thirdPartyErrorMessage,
76+
} = thirdPartyAuthContext;
7177
const { formatMessage } = useIntl();
7278
const activationMsgType = getActivationStatus();
7379
const queryParams = useMemo(() => getAllPossibleQueryParams(), []);
7480

7581
const [formFields, setFormFields] = useState({ ...backedUpFormData.formFields });
76-
const [errorCode, setErrorCode] = useState({ type: '', count: 0, context: {} });
82+
const [errorCode, setErrorCode] = useState({
83+
type: '',
84+
count: 0,
85+
context: {},
86+
});
7787
const [errors, setErrors] = useState({ ...backedUpFormData.errors });
7888
const tpaHint = getTpaHint();
7989

@@ -87,7 +97,7 @@ const LoginPage = (props) => {
8797
payload.tpa_hint = tpaHint;
8898
}
8999
getTPADataFromBackend(payload);
90-
}, [getTPADataFromBackend, queryParams, tpaHint]);
100+
}, [queryParams, tpaHint, getTPADataFromBackend]);
91101
/**
92102
* Backup the login form in redux when login page is toggled.
93103
*/
@@ -98,7 +108,7 @@ const LoginPage = (props) => {
98108
errors: { ...errors },
99109
});
100110
}
101-
}, [shouldBackupState, formFields, errors, backupFormState]);
111+
}, [backupFormState, shouldBackupState, formFields, errors]);
102112

103113
useEffect(() => {
104114
if (loginErrorCode) {
@@ -123,7 +133,10 @@ const LoginPage = (props) => {
123133
}, [thirdPartyErrorMessage]);
124134

125135
const validateFormFields = (payload) => {
126-
const { emailOrUsername, password } = payload;
136+
const {
137+
emailOrUsername,
138+
password,
139+
} = payload;
127140
const fieldErrors = { ...errors };
128141

129142
if (emailOrUsername === '') {
@@ -141,14 +154,18 @@ const LoginPage = (props) => {
141154
const handleSubmit = (event) => {
142155
event.preventDefault();
143156
if (showResetPasswordSuccessBanner) {
144-
props.dismissPasswordResetBanner();
157+
dispatch(dismissPasswordResetBanner());
145158
}
146159

147160
const formData = { ...formFields };
148161
const validationErrors = validateFormFields(formData);
149162
if (validationErrors.emailOrUsername || validationErrors.password) {
150163
setErrors({ ...validationErrors });
151-
setErrorCode(prevState => ({ type: INVALID_FORM, count: prevState.count + 1, context: {} }));
164+
setErrorCode(prevState => ({
165+
type: INVALID_FORM,
166+
count: prevState.count + 1,
167+
context: {},
168+
}));
152169
return;
153170
}
154171

@@ -158,23 +175,35 @@ const LoginPage = (props) => {
158175
password: formData.password,
159176
...queryParams,
160177
};
161-
props.loginRequest(payload);
178+
dispatch(loginRequest(payload));
162179
};
163180

164181
const handleOnChange = (event) => {
165-
const { name, value } = event.target;
166-
setFormFields(prevState => ({ ...prevState, [name]: value }));
182+
const {
183+
name,
184+
value,
185+
} = event.target;
186+
setFormFields(prevState => ({
187+
...prevState,
188+
[name]: value,
189+
}));
167190
};
168191

169192
const handleOnFocus = (event) => {
170193
const { name } = event.target;
171-
setErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
194+
setErrors(prevErrors => ({
195+
...prevErrors,
196+
[name]: '',
197+
}));
172198
};
173199
const trackForgotPasswordLinkClick = () => {
174200
sendTrackEvent('edx.bi.password-reset_form.toggled', { category: 'user-engagement' });
175201
};
176202

177-
const { provider, skipHintedLogin } = getTpaProvider(tpaHint, providers, secondaryProviders);
203+
const {
204+
provider,
205+
skipHintedLogin,
206+
} = getTpaProvider(tpaHint, providers, secondaryProviders);
178207

179208
if (tpaHint) {
180209
if (thirdPartyAuthApiStatus === PENDING_STATE) {
@@ -281,88 +310,9 @@ const LoginPage = (props) => {
281310
);
282311
};
283312

284-
const mapStateToProps = state => {
285-
const loginPageState = state.login;
286-
return {
287-
backedUpFormData: loginPageState.loginFormData,
288-
loginErrorCode: loginPageState.loginErrorCode,
289-
loginErrorContext: loginPageState.loginErrorContext,
290-
loginResult: loginPageState.loginResult,
291-
shouldBackupState: loginPageState.shouldBackupState,
292-
showResetPasswordSuccessBanner: loginPageState.showResetPasswordSuccessBanner,
293-
submitState: loginPageState.submitState,
294-
thirdPartyAuthContext: thirdPartyAuthContextSelector(state),
295-
thirdPartyAuthApiStatus: state.commonComponents.thirdPartyAuthApiStatus,
296-
};
297-
};
298-
299313
LoginPage.propTypes = {
300-
backedUpFormData: PropTypes.shape({
301-
formFields: PropTypes.shape({}),
302-
errors: PropTypes.shape({}),
303-
}),
304-
loginErrorCode: PropTypes.string,
305-
loginErrorContext: PropTypes.shape({
306-
email: PropTypes.string,
307-
redirectUrl: PropTypes.string,
308-
context: PropTypes.shape({}),
309-
}),
310-
loginResult: PropTypes.shape({
311-
redirectUrl: PropTypes.string,
312-
success: PropTypes.bool,
313-
}),
314-
shouldBackupState: PropTypes.bool,
315-
showResetPasswordSuccessBanner: PropTypes.bool,
316-
submitState: PropTypes.string,
317-
thirdPartyAuthApiStatus: PropTypes.string,
318314
institutionLogin: PropTypes.bool.isRequired,
319-
thirdPartyAuthContext: PropTypes.shape({
320-
currentProvider: PropTypes.string,
321-
errorMessage: PropTypes.string,
322-
platformName: PropTypes.string,
323-
providers: PropTypes.arrayOf(PropTypes.shape({})),
324-
secondaryProviders: PropTypes.arrayOf(PropTypes.shape({})),
325-
finishAuthUrl: PropTypes.string,
326-
}),
327-
// Actions
328-
backupFormState: PropTypes.func.isRequired,
329-
dismissPasswordResetBanner: PropTypes.func.isRequired,
330-
loginRequest: PropTypes.func.isRequired,
331-
getTPADataFromBackend: PropTypes.func.isRequired,
332315
handleInstitutionLogin: PropTypes.func.isRequired,
333316
};
334317

335-
LoginPage.defaultProps = {
336-
backedUpFormData: {
337-
formFields: {
338-
emailOrUsername: '', password: '',
339-
},
340-
errors: {
341-
emailOrUsername: '', password: '',
342-
},
343-
},
344-
loginErrorCode: null,
345-
loginErrorContext: {},
346-
loginResult: {},
347-
shouldBackupState: false,
348-
showResetPasswordSuccessBanner: false,
349-
submitState: DEFAULT_STATE,
350-
thirdPartyAuthApiStatus: PENDING_STATE,
351-
thirdPartyAuthContext: {
352-
currentProvider: null,
353-
errorMessage: null,
354-
finishAuthUrl: null,
355-
providers: [],
356-
secondaryProviders: [],
357-
},
358-
};
359-
360-
export default connect(
361-
mapStateToProps,
362-
{
363-
backupFormState: backupLoginFormBegin,
364-
dismissPasswordResetBanner,
365-
loginRequest,
366-
getTPADataFromBackend: getThirdPartyAuthContext,
367-
},
368-
)(LoginPage);
318+
export default LoginPage;

src/login/tests/LoginPage.test.jsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,14 @@ describe('LoginPage', () => {
4141
const initialState = {
4242
login: {
4343
loginResult: { success: false, redirectUrl: '' },
44+
loginFormData: {
45+
formFields: {
46+
emailOrUsername: '', password: '',
47+
},
48+
errors: {
49+
emailOrUsername: '', password: '',
50+
},
51+
},
4452
},
4553
commonComponents: {
4654
thirdPartyAuthApiStatus: null,

0 commit comments

Comments
 (0)