@openedx/frontend-app-authn 1.0.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.rst +204 -0
- package/package.json +79 -0
- package/src/Main.tsx +23 -0
- package/src/__mocks__/file.js +1 -0
- package/src/__mocks__/svg.js +1 -0
- package/src/app.scss +6 -0
- package/src/app.ts +43 -0
- package/src/base-container/components/default-layout/DefaultLayout.test.jsx +48 -0
- package/src/base-container/components/default-layout/LargeLayout.jsx +42 -0
- package/src/base-container/components/default-layout/MediumLayout.jsx +49 -0
- package/src/base-container/components/default-layout/SmallLayout.jsx +37 -0
- package/src/base-container/components/default-layout/index.jsx +3 -0
- package/src/base-container/components/default-layout/messages.js +16 -0
- package/src/base-container/components/image-layout/ExtraSmallLayout.jsx +31 -0
- package/src/base-container/components/image-layout/LargeLayout.jsx +32 -0
- package/src/base-container/components/image-layout/MediumLayout.jsx +32 -0
- package/src/base-container/components/image-layout/SmallLayout.jsx +31 -0
- package/src/base-container/components/image-layout/index.jsx +4 -0
- package/src/base-container/components/image-layout/index.scss +37 -0
- package/src/base-container/components/image-layout/messages.js +16 -0
- package/src/base-container/components/welcome-page-layout/LargeLayout.jsx +46 -0
- package/src/base-container/components/welcome-page-layout/MediumLayout.jsx +49 -0
- package/src/base-container/components/welcome-page-layout/SmallLayout.jsx +38 -0
- package/src/base-container/components/welcome-page-layout/index.jsx +3 -0
- package/src/base-container/components/welcome-page-layout/messages.js +21 -0
- package/src/base-container/index.jsx +70 -0
- package/src/base-container/tests/BaseContainer.test.jsx +44 -0
- package/src/common-components/EmbeddedRegistrationRoute.jsx +26 -0
- package/src/common-components/EnterpriseSSO.jsx +110 -0
- package/src/common-components/FormGroup.jsx +106 -0
- package/src/common-components/InstitutionLogistration.jsx +102 -0
- package/src/common-components/NotFoundPage.jsx +15 -0
- package/src/common-components/PasswordField.jsx +175 -0
- package/src/common-components/RedirectLogistration.jsx +94 -0
- package/src/common-components/RegisterFaIcons.jsx +8 -0
- package/src/common-components/SocialAuthProviders.jsx +75 -0
- package/src/common-components/ThirdPartyAuth.jsx +123 -0
- package/src/common-components/ThirdPartyAuthAlert.jsx +49 -0
- package/src/common-components/UnAuthOnlyRoute.jsx +41 -0
- package/src/common-components/data/actions.js +27 -0
- package/src/common-components/data/reducers.js +63 -0
- package/src/common-components/data/sagas.js +32 -0
- package/src/common-components/data/selectors.js +28 -0
- package/src/common-components/data/service.js +23 -0
- package/src/common-components/data/tests/reducer.test.js +82 -0
- package/src/common-components/data/tests/sagas.test.js +71 -0
- package/src/common-components/index.jsx +14 -0
- package/src/common-components/messages.jsx +127 -0
- package/src/common-components/tests/EmbeddedRegistrationRoute.test.jsx +76 -0
- package/src/common-components/tests/FormField.test.jsx +289 -0
- package/src/common-components/tests/SocialAuthProviders.test.jsx +75 -0
- package/src/common-components/tests/ThirdPartyAuthAlert.test.jsx +39 -0
- package/src/common-components/tests/UnAuthOnlyRoute.test.jsx +73 -0
- package/src/common-components/tests/__snapshots__/SocialAuthProviders.test.jsx.snap +156 -0
- package/src/common-components/tests/__snapshots__/ThirdPartyAuthAlert.test.jsx.snap +53 -0
- package/src/constants.ts +1 -0
- package/src/data/configureStore.js +33 -0
- package/src/data/constants.js +38 -0
- package/src/data/countries.ts +59 -0
- package/src/data/oneTrust.js +3 -0
- package/src/data/reducers.js +36 -0
- package/src/data/sagas.js +19 -0
- package/src/data/tests/cookies.test.js +42 -0
- package/src/data/tests/dataUtils.test.js +19 -0
- package/src/data/tests/reduxUtils.test.js +14 -0
- package/src/data/utils/cookies.js +12 -0
- package/src/data/utils/dataUtils.js +81 -0
- package/src/data/utils/index.js +11 -0
- package/src/data/utils/reduxUtils.js +34 -0
- package/src/data/utils/useMobileResponsive.js +24 -0
- package/src/field-renderer/FieldRenderer.jsx +156 -0
- package/src/field-renderer/index.jsx +2 -0
- package/src/field-renderer/tests/FieldRenderer.test.jsx +201 -0
- package/src/forgot-password/ForgotPasswordAlert.jsx +97 -0
- package/src/forgot-password/ForgotPasswordPage.jsx +193 -0
- package/src/forgot-password/data/actions.js +32 -0
- package/src/forgot-password/data/reducers.js +58 -0
- package/src/forgot-password/data/sagas.js +35 -0
- package/src/forgot-password/data/selectors.js +10 -0
- package/src/forgot-password/data/service.js +21 -0
- package/src/forgot-password/data/tests/reducers.test.js +34 -0
- package/src/forgot-password/data/tests/sagas.test.js +67 -0
- package/src/forgot-password/index.js +5 -0
- package/src/forgot-password/messages.js +128 -0
- package/src/forgot-password/tests/ForgotPasswordPage.test.jsx +269 -0
- package/src/i18n/index.js +25 -0
- package/src/index.ts +3 -0
- package/src/login/AccountActivationMessage.jsx +78 -0
- package/src/login/ChangePasswordPrompt.jsx +91 -0
- package/src/login/LoginFailure.jsx +231 -0
- package/src/login/LoginPage.jsx +368 -0
- package/src/login/data/actions.js +39 -0
- package/src/login/data/constants.js +20 -0
- package/src/login/data/reducers.js +76 -0
- package/src/login/data/sagas.js +45 -0
- package/src/login/data/service.js +24 -0
- package/src/login/data/tests/reducers.test.js +155 -0
- package/src/login/data/tests/sagas.test.js +110 -0
- package/src/login/index.js +5 -0
- package/src/login/messages.jsx +211 -0
- package/src/login/tests/AccountActivationMessage.test.jsx +122 -0
- package/src/login/tests/ChangePasswordPrompt.test.jsx +75 -0
- package/src/login/tests/LoginFailure.test.jsx +323 -0
- package/src/login/tests/LoginPage.test.jsx +839 -0
- package/src/logistration/Logistration.jsx +189 -0
- package/src/logistration/Logistration.test.jsx +275 -0
- package/src/progressive-profiling/ProgressiveProfiling.jsx +313 -0
- package/src/progressive-profiling/ProgressiveProfilingPageModal.jsx +56 -0
- package/src/progressive-profiling/data/actions.js +22 -0
- package/src/progressive-profiling/data/reducers.js +38 -0
- package/src/progressive-profiling/data/sagas.js +24 -0
- package/src/progressive-profiling/data/selectors.js +14 -0
- package/src/progressive-profiling/data/service.js +17 -0
- package/src/progressive-profiling/index.js +5 -0
- package/src/progressive-profiling/messages.jsx +57 -0
- package/src/progressive-profiling/tests/ProgressiveProfiling.test.jsx +322 -0
- package/src/register/RegistrationFields/CountryField/CountryField.jsx +164 -0
- package/src/register/RegistrationFields/CountryField/CountryField.test.jsx +204 -0
- package/src/register/RegistrationFields/CountryField/validator.js +32 -0
- package/src/register/RegistrationFields/EmailField/EmailField.jsx +140 -0
- package/src/register/RegistrationFields/EmailField/EmailField.test.jsx +236 -0
- package/src/register/RegistrationFields/EmailField/constants.js +131 -0
- package/src/register/RegistrationFields/EmailField/validator.js +126 -0
- package/src/register/RegistrationFields/EmailField/validator.test.js +39 -0
- package/src/register/RegistrationFields/HonorCodeField/HonorCode.jsx +104 -0
- package/src/register/RegistrationFields/HonorCodeField/HonorCode.test.jsx +70 -0
- package/src/register/RegistrationFields/NameField/NameField.jsx +68 -0
- package/src/register/RegistrationFields/NameField/NameField.test.jsx +146 -0
- package/src/register/RegistrationFields/NameField/validator.js +22 -0
- package/src/register/RegistrationFields/TermsOfServiceField/TermsOfService.jsx +58 -0
- package/src/register/RegistrationFields/TermsOfServiceField/TermsOfService.test.jsx +55 -0
- package/src/register/RegistrationFields/UsernameField/UsernameField.jsx +162 -0
- package/src/register/RegistrationFields/UsernameField/UsernameField.test.jsx +319 -0
- package/src/register/RegistrationFields/UsernameField/validator.js +16 -0
- package/src/register/RegistrationFields/index.js +6 -0
- package/src/register/RegistrationPage.jsx +428 -0
- package/src/register/RegistrationPage.test.jsx +854 -0
- package/src/register/components/ConfigurableRegistrationForm.jsx +244 -0
- package/src/register/components/RegistrationFailure.jsx +80 -0
- package/src/register/components/tests/ConfigurableRegistrationForm.test.jsx +401 -0
- package/src/register/components/tests/RegistrationFailure.test.jsx +212 -0
- package/src/register/components/tests/ThirdPartyAuth.test.jsx +439 -0
- package/src/register/data/actions.js +85 -0
- package/src/register/data/constants.js +13 -0
- package/src/register/data/reducers.js +140 -0
- package/src/register/data/sagas.js +58 -0
- package/src/register/data/selectors.js +34 -0
- package/src/register/data/service.js +45 -0
- package/src/register/data/tests/reducers.test.js +277 -0
- package/src/register/data/tests/sagas.test.js +239 -0
- package/src/register/data/tests/utils.test.js +98 -0
- package/src/register/data/utils.js +153 -0
- package/src/register/index.js +4 -0
- package/src/register/messages.jsx +206 -0
- package/src/reset-password/ResetPasswordFailure.jsx +55 -0
- package/src/reset-password/ResetPasswordPage.jsx +224 -0
- package/src/reset-password/ResetPasswordSuccess.jsx +19 -0
- package/src/reset-password/data/actions.js +50 -0
- package/src/reset-password/data/constants.js +16 -0
- package/src/reset-password/data/reducers.js +44 -0
- package/src/reset-password/data/sagas.js +67 -0
- package/src/reset-password/data/selectors.js +10 -0
- package/src/reset-password/data/service.js +61 -0
- package/src/reset-password/data/tests/sagas.test.js +185 -0
- package/src/reset-password/index.js +5 -0
- package/src/reset-password/messages.js +88 -0
- package/src/reset-password/tests/ResetPasswordPage.test.jsx +241 -0
- package/src/routes.jsx +79 -0
- package/src/sass/_base_component.scss +202 -0
- package/src/sass/_forgot_password.scss +3 -0
- package/src/sass/_login_page.scss +3 -0
- package/src/sass/_progressive_profiling_page.scss +30 -0
- package/src/sass/_registration.scss +123 -0
- package/src/sass/_reset_password.scss +3 -0
- package/src/sass/_style.scss +465 -0
- package/src/setupTest.js +68 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { getSiteConfig } from '@openedx/frontend-base';
|
|
2
|
+
import { composeWithDevTools } from '@redux-devtools/extension';
|
|
3
|
+
import { applyMiddleware, compose, createStore } from 'redux';
|
|
4
|
+
import { createLogger } from 'redux-logger';
|
|
5
|
+
import createSagaMiddleware from 'redux-saga';
|
|
6
|
+
import thunkMiddleware from 'redux-thunk';
|
|
7
|
+
|
|
8
|
+
import createRootReducer from './reducers';
|
|
9
|
+
import rootSaga from './sagas';
|
|
10
|
+
|
|
11
|
+
const sagaMiddleware = createSagaMiddleware();
|
|
12
|
+
|
|
13
|
+
function composeMiddleware() {
|
|
14
|
+
if (getSiteConfig().environment === 'development') {
|
|
15
|
+
const loggerMiddleware = createLogger({
|
|
16
|
+
collapsed: true,
|
|
17
|
+
});
|
|
18
|
+
return composeWithDevTools(applyMiddleware(thunkMiddleware, sagaMiddleware, loggerMiddleware));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return compose(applyMiddleware(thunkMiddleware, sagaMiddleware));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default function configureStore(initialState = {}) {
|
|
25
|
+
const store = createStore(
|
|
26
|
+
createRootReducer(),
|
|
27
|
+
initialState,
|
|
28
|
+
composeMiddleware(),
|
|
29
|
+
);
|
|
30
|
+
sagaMiddleware.run(rootSaga);
|
|
31
|
+
|
|
32
|
+
return store;
|
|
33
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// URL Paths
|
|
2
|
+
export const LOGIN_PAGE = '/login';
|
|
3
|
+
export const REGISTER_PAGE = '/register';
|
|
4
|
+
export const REGISTER_EMBEDDED_PAGE = '/register-embedded';
|
|
5
|
+
export const RESET_PAGE = '/reset';
|
|
6
|
+
export const AUTHN_PROGRESSIVE_PROFILING = '/welcome';
|
|
7
|
+
export const DEFAULT_REDIRECT_URL = '/dashboard';
|
|
8
|
+
export const PASSWORD_RESET_CONFIRM = '/password_reset_confirm/:token/';
|
|
9
|
+
export const PAGE_NOT_FOUND = '/notfound';
|
|
10
|
+
export const ENTERPRISE_LOGIN_URL = '/enterprise/login';
|
|
11
|
+
|
|
12
|
+
// Constants
|
|
13
|
+
export const SUPPORTED_ICON_CLASSES = ['apple', 'facebook', 'google', 'microsoft'];
|
|
14
|
+
|
|
15
|
+
// Error Codes
|
|
16
|
+
export const FORM_SUBMISSION_ERROR = 'form-submission-error';
|
|
17
|
+
export const INTERNAL_SERVER_ERROR = 'internal-server-error';
|
|
18
|
+
export const API_RATELIMIT_ERROR = 'api-ratelimit-error';
|
|
19
|
+
|
|
20
|
+
// Common States
|
|
21
|
+
export const DEFAULT_STATE = 'default';
|
|
22
|
+
export const PENDING_STATE = 'pending';
|
|
23
|
+
export const COMPLETE_STATE = 'complete';
|
|
24
|
+
export const FAILURE_STATE = 'failure';
|
|
25
|
+
export const FORBIDDEN_STATE = 'forbidden';
|
|
26
|
+
export const EMBEDDED = 'embedded';
|
|
27
|
+
|
|
28
|
+
export const LETTER_REGEX = /[a-zA-Z]/;
|
|
29
|
+
export const NUMBER_REGEX = /\d/;
|
|
30
|
+
export const VALID_EMAIL_REGEX = '(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+)*'
|
|
31
|
+
+ '|^"([\\001-\\010\\013\\014\\016-\\037!#-\\[\\]-\\177]|\\\\[\\001-\\011\\013\\014\\016-\\177])*"'
|
|
32
|
+
+ ')@((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\\.)+)(?:[A-Z0-9-]{2,63})'
|
|
33
|
+
+ '|\\[(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\]$';
|
|
34
|
+
|
|
35
|
+
// Query string parameters that can be passed to LMS to manage
|
|
36
|
+
// things like auto-enrollment upon login and registration.
|
|
37
|
+
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'];
|
|
38
|
+
export const REDIRECT = 'redirect';
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { getPrimaryLanguageSubtag } from '@openedx/frontend-base';
|
|
2
|
+
import COUNTRIES, { langs as countryLangs } from 'i18n-iso-countries';
|
|
3
|
+
import arLocale from 'i18n-iso-countries/langs/ar.json';
|
|
4
|
+
import caLocale from 'i18n-iso-countries/langs/ca.json';
|
|
5
|
+
import enLocale from 'i18n-iso-countries/langs/en.json';
|
|
6
|
+
import esLocale from 'i18n-iso-countries/langs/es.json';
|
|
7
|
+
import frLocale from 'i18n-iso-countries/langs/fr.json';
|
|
8
|
+
import heLocale from 'i18n-iso-countries/langs/he.json';
|
|
9
|
+
import idLocale from 'i18n-iso-countries/langs/id.json';
|
|
10
|
+
import koLocale from 'i18n-iso-countries/langs/ko.json';
|
|
11
|
+
import plLocale from 'i18n-iso-countries/langs/pl.json';
|
|
12
|
+
import ptLocale from 'i18n-iso-countries/langs/pt.json';
|
|
13
|
+
import ruLocale from 'i18n-iso-countries/langs/ru.json';
|
|
14
|
+
import ukLocale from 'i18n-iso-countries/langs/uk.json';
|
|
15
|
+
import zhLocale from 'i18n-iso-countries/langs/zh.json';
|
|
16
|
+
|
|
17
|
+
COUNTRIES.registerLocale(arLocale);
|
|
18
|
+
COUNTRIES.registerLocale(enLocale);
|
|
19
|
+
COUNTRIES.registerLocale(esLocale);
|
|
20
|
+
COUNTRIES.registerLocale(frLocale);
|
|
21
|
+
COUNTRIES.registerLocale(zhLocale);
|
|
22
|
+
COUNTRIES.registerLocale(caLocale);
|
|
23
|
+
COUNTRIES.registerLocale(heLocale);
|
|
24
|
+
COUNTRIES.registerLocale(idLocale);
|
|
25
|
+
COUNTRIES.registerLocale(koLocale);
|
|
26
|
+
COUNTRIES.registerLocale(plLocale);
|
|
27
|
+
COUNTRIES.registerLocale(ptLocale);
|
|
28
|
+
COUNTRIES.registerLocale(ruLocale);
|
|
29
|
+
COUNTRIES.registerLocale(ukLocale);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Provides a lookup table of country IDs to country names for the current locale.
|
|
33
|
+
*
|
|
34
|
+
* @memberof module:I18n
|
|
35
|
+
*/
|
|
36
|
+
export function getCountryMessages(locale) {
|
|
37
|
+
const primaryLanguageSubtag = getPrimaryLanguageSubtag(locale);
|
|
38
|
+
const languageCode = countryLangs().includes(primaryLanguageSubtag) ? primaryLanguageSubtag : 'en';
|
|
39
|
+
|
|
40
|
+
return COUNTRIES.getNames(languageCode);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Provides a list of countries represented as objects of the following shape:
|
|
45
|
+
*
|
|
46
|
+
* {
|
|
47
|
+
* key, // The ID of the country
|
|
48
|
+
* name // The localized name of the country
|
|
49
|
+
* }
|
|
50
|
+
*
|
|
51
|
+
* TODO: ARCH-878: The list should be sorted alphabetically in the current locale.
|
|
52
|
+
* This is useful for populating dropdowns.
|
|
53
|
+
*
|
|
54
|
+
* @memberof module:I18n
|
|
55
|
+
*/
|
|
56
|
+
export function getCountryList(locale) {
|
|
57
|
+
const countryMessages = getCountryMessages(locale);
|
|
58
|
+
return Object.entries(countryMessages).map(([code, name]) => ({ code, name }));
|
|
59
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { combineReducers } from 'redux';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
reducer as commonComponentsReducer,
|
|
5
|
+
storeName as commonComponentsStoreName,
|
|
6
|
+
} from '../common-components';
|
|
7
|
+
import {
|
|
8
|
+
reducer as forgotPasswordReducer,
|
|
9
|
+
storeName as forgotPasswordStoreName,
|
|
10
|
+
} from '../forgot-password';
|
|
11
|
+
import {
|
|
12
|
+
reducer as loginReducer,
|
|
13
|
+
storeName as loginStoreName,
|
|
14
|
+
} from '../login';
|
|
15
|
+
import {
|
|
16
|
+
reducer as authnProgressiveProfilingReducers,
|
|
17
|
+
storeName as authnProgressiveProfilingStoreName,
|
|
18
|
+
} from '../progressive-profiling';
|
|
19
|
+
import {
|
|
20
|
+
reducer as registerReducer,
|
|
21
|
+
storeName as registerStoreName,
|
|
22
|
+
} from '../register';
|
|
23
|
+
import {
|
|
24
|
+
reducer as resetPasswordReducer,
|
|
25
|
+
storeName as resetPasswordStoreName,
|
|
26
|
+
} from '../reset-password';
|
|
27
|
+
|
|
28
|
+
const createRootReducer = () => combineReducers({
|
|
29
|
+
[loginStoreName]: loginReducer,
|
|
30
|
+
[registerStoreName]: registerReducer,
|
|
31
|
+
[commonComponentsStoreName]: commonComponentsReducer,
|
|
32
|
+
[forgotPasswordStoreName]: forgotPasswordReducer,
|
|
33
|
+
[resetPasswordStoreName]: resetPasswordReducer,
|
|
34
|
+
[authnProgressiveProfilingStoreName]: authnProgressiveProfilingReducers,
|
|
35
|
+
});
|
|
36
|
+
export default createRootReducer;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { all } from 'redux-saga/effects';
|
|
2
|
+
|
|
3
|
+
import { saga as commonComponentsSaga } from '../common-components';
|
|
4
|
+
import { saga as forgotPasswordSaga } from '../forgot-password';
|
|
5
|
+
import { saga as loginSaga } from '../login';
|
|
6
|
+
import { saga as authnProgressiveProfilingSaga } from '../progressive-profiling';
|
|
7
|
+
import { saga as registrationSaga } from '../register';
|
|
8
|
+
import { saga as resetPasswordSaga } from '../reset-password';
|
|
9
|
+
|
|
10
|
+
export default function* rootSaga() {
|
|
11
|
+
yield all([
|
|
12
|
+
loginSaga(),
|
|
13
|
+
registrationSaga(),
|
|
14
|
+
commonComponentsSaga(),
|
|
15
|
+
forgotPasswordSaga(),
|
|
16
|
+
resetPasswordSaga(),
|
|
17
|
+
authnProgressiveProfilingSaga(),
|
|
18
|
+
]);
|
|
19
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import Cookies from 'universal-cookie';
|
|
2
|
+
|
|
3
|
+
import { setCookie } from '../utils';
|
|
4
|
+
|
|
5
|
+
// Mock Cookies class
|
|
6
|
+
jest.mock('universal-cookie');
|
|
7
|
+
|
|
8
|
+
describe('setCookie function', () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
jest.clearAllMocks();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should set a cookie with default options', () => {
|
|
14
|
+
setCookie('testCookie', 'testValue', 'example.com');
|
|
15
|
+
|
|
16
|
+
expect(Cookies).toHaveBeenCalled();
|
|
17
|
+
expect(Cookies).toHaveBeenCalledWith();
|
|
18
|
+
expect(Cookies.prototype.set).toHaveBeenCalledWith('testCookie', 'testValue', {
|
|
19
|
+
domain: 'example.com',
|
|
20
|
+
path: '/',
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should set a cookie with specified expiry', () => {
|
|
25
|
+
const expiry = new Date('2023-12-31');
|
|
26
|
+
setCookie('testCookie', 'testValue', 'example.com', expiry);
|
|
27
|
+
|
|
28
|
+
expect(Cookies).toHaveBeenCalled();
|
|
29
|
+
expect(Cookies).toHaveBeenCalledWith();
|
|
30
|
+
expect(Cookies.prototype.set).toHaveBeenCalledWith('testCookie', 'testValue', {
|
|
31
|
+
domain: 'example.com',
|
|
32
|
+
path: '/',
|
|
33
|
+
expires: expiry,
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should not set a cookie if cookieName is undefined', () => {
|
|
38
|
+
setCookie(undefined, 'testValue', 'example.com');
|
|
39
|
+
|
|
40
|
+
expect(Cookies).not.toHaveBeenCalled();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { LOGIN_PAGE } from '../constants';
|
|
2
|
+
import { updatePathWithQueryParams } from '../utils/dataUtils';
|
|
3
|
+
|
|
4
|
+
describe('updatePathWithQueryParams', () => {
|
|
5
|
+
it('should append query params into the path', () => {
|
|
6
|
+
const params = '?course_id=testCourseId';
|
|
7
|
+
const expectedPath = `${LOGIN_PAGE}${params}`;
|
|
8
|
+
|
|
9
|
+
Object.defineProperty(window, 'location', {
|
|
10
|
+
value: {
|
|
11
|
+
href: 'http://localhost/',
|
|
12
|
+
search: params,
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
const updatedPath = updatePathWithQueryParams(LOGIN_PAGE);
|
|
16
|
+
|
|
17
|
+
expect(updatedPath).toEqual(expectedPath);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import AsyncActionType from '../utils/reduxUtils';
|
|
2
|
+
|
|
3
|
+
describe('AsyncActionType', () => {
|
|
4
|
+
it('should return well formatted action strings', () => {
|
|
5
|
+
const actionType = new AsyncActionType('HOUSE_CATS', 'START_THE_RACE');
|
|
6
|
+
|
|
7
|
+
expect(actionType.BASE).toBe('HOUSE_CATS__START_THE_RACE');
|
|
8
|
+
expect(actionType.BEGIN).toBe('HOUSE_CATS__START_THE_RACE__BEGIN');
|
|
9
|
+
expect(actionType.SUCCESS).toBe('HOUSE_CATS__START_THE_RACE__SUCCESS');
|
|
10
|
+
expect(actionType.FAILURE).toBe('HOUSE_CATS__START_THE_RACE__FAILURE');
|
|
11
|
+
expect(actionType.RESET).toBe('HOUSE_CATS__START_THE_RACE__RESET');
|
|
12
|
+
expect(actionType.FORBIDDEN).toBe('HOUSE_CATS__START_THE_RACE__FORBIDDEN');
|
|
13
|
+
});
|
|
14
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import Cookies from 'universal-cookie';
|
|
2
|
+
|
|
3
|
+
export default function setCookie(cookieName, cookieValue, cookieDomain, cookieExpiry) {
|
|
4
|
+
if (cookieName) { // To avoid setting getting exception when setting cookie with undefined names.
|
|
5
|
+
const cookies = new Cookies();
|
|
6
|
+
const options = { domain: cookieDomain, path: '/' };
|
|
7
|
+
if (cookieExpiry) {
|
|
8
|
+
options.expires = cookieExpiry;
|
|
9
|
+
}
|
|
10
|
+
cookies.set(cookieName, cookieValue, options);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Utility functions
|
|
2
|
+
import * as QueryString from 'query-string';
|
|
3
|
+
|
|
4
|
+
import { AUTH_PARAMS } from '../constants';
|
|
5
|
+
|
|
6
|
+
export const getTpaProvider = (tpaHintProvider, primaryProviders, secondaryProviders) => {
|
|
7
|
+
let tpaProvider = null;
|
|
8
|
+
let skipHintedLogin = false;
|
|
9
|
+
[...primaryProviders, ...secondaryProviders].forEach((provider) => {
|
|
10
|
+
if (provider.id === tpaHintProvider) {
|
|
11
|
+
tpaProvider = provider;
|
|
12
|
+
if (provider.skipHintedLogin) {
|
|
13
|
+
skipHintedLogin = true;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
return { provider: tpaProvider, skipHintedLogin };
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const getTpaHint = () => {
|
|
21
|
+
const params = QueryString.parse(window.location.search);
|
|
22
|
+
let tpaHint = null;
|
|
23
|
+
tpaHint = params.tpa_hint;
|
|
24
|
+
if (!tpaHint) {
|
|
25
|
+
const { next } = params;
|
|
26
|
+
if (next) {
|
|
27
|
+
const index = next.indexOf('tpa_hint=');
|
|
28
|
+
if (index !== -1) {
|
|
29
|
+
tpaHint = next.substring(index + 'tpa_hint='.length, next.length);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return tpaHint;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const updatePathWithQueryParams = (path) => {
|
|
37
|
+
let queryParams = window.location.search;
|
|
38
|
+
|
|
39
|
+
if (!queryParams) {
|
|
40
|
+
return path;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (queryParams.includes('track=pwreset')) {
|
|
44
|
+
queryParams = queryParams.replace('?track=pwreset&', '?',).replace('?track=pwreset', '').replace('&track=pwreset', '').replace('?&', '?');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return `${path}${queryParams}`;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const getAllPossibleQueryParams = (locationURl = null) => {
|
|
51
|
+
const urlParams = locationURl ? QueryString.parseUrl(locationURl).query : QueryString.parse(window.location.search);
|
|
52
|
+
const params = {};
|
|
53
|
+
Object.entries(urlParams).forEach(([key, value]) => {
|
|
54
|
+
if (AUTH_PARAMS.includes(key)) {
|
|
55
|
+
params[key] = value;
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return params;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const getActivationStatus = () => {
|
|
63
|
+
const params = QueryString.parse(window.location.search);
|
|
64
|
+
|
|
65
|
+
return params.account_activation_status;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const isScrollBehaviorSupported = () => 'scrollBehavior' in document.documentElement.style;
|
|
69
|
+
|
|
70
|
+
export const windowScrollTo = (options) => {
|
|
71
|
+
if (isScrollBehaviorSupported()) {
|
|
72
|
+
return window.scrollTo(options);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return window.scrollTo(options.top, options.left);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const isHostAvailableInQueryParams = () => {
|
|
79
|
+
const queryParams = getAllPossibleQueryParams();
|
|
80
|
+
return 'host' in queryParams;
|
|
81
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export {
|
|
2
|
+
getTpaProvider,
|
|
3
|
+
getTpaHint,
|
|
4
|
+
getAllPossibleQueryParams,
|
|
5
|
+
getActivationStatus,
|
|
6
|
+
isHostAvailableInQueryParams,
|
|
7
|
+
updatePathWithQueryParams,
|
|
8
|
+
windowScrollTo,
|
|
9
|
+
} from './dataUtils';
|
|
10
|
+
export { default as AsyncActionType } from './reduxUtils';
|
|
11
|
+
export { default as setCookie } from './cookies';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper class to save time when writing out action types for asynchronous methods. Also helps
|
|
3
|
+
* ensure that actions are namespaced.
|
|
4
|
+
*/
|
|
5
|
+
export default class AsyncActionType {
|
|
6
|
+
constructor(topic, name) {
|
|
7
|
+
this.topic = topic;
|
|
8
|
+
this.name = name;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
get BASE() {
|
|
12
|
+
return `${this.topic}__${this.name}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
get BEGIN() {
|
|
16
|
+
return `${this.topic}__${this.name}__BEGIN`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get SUCCESS() {
|
|
20
|
+
return `${this.topic}__${this.name}__SUCCESS`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get FAILURE() {
|
|
24
|
+
return `${this.topic}__${this.name}__FAILURE`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get RESET() {
|
|
28
|
+
return `${this.topic}__${this.name}__RESET`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get FORBIDDEN() {
|
|
32
|
+
return `${this.topic}__${this.name}__FORBIDDEN`;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { breakpoints } from '@openedx/paragon';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A react hook used to determine if the current window is mobile or not.
|
|
7
|
+
* returns true if the window is of mobile size.
|
|
8
|
+
* Code source: https://github.com/edx/prospectus/blob/master/src/utils/useMobileResponsive.js
|
|
9
|
+
*/
|
|
10
|
+
const useMobileResponsive = (breakpoint) => {
|
|
11
|
+
const [isMobileWindow, setIsMobileWindow] = useState();
|
|
12
|
+
const checkForMobile = () => {
|
|
13
|
+
setIsMobileWindow(window.matchMedia(`(max-width: ${breakpoint || breakpoints.small.maxWidth}px)`).matches);
|
|
14
|
+
};
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
checkForMobile();
|
|
17
|
+
window.addEventListener('resize', checkForMobile);
|
|
18
|
+
// return this function here to clean up the event listener
|
|
19
|
+
return () => window.removeEventListener('resize', checkForMobile);
|
|
20
|
+
});
|
|
21
|
+
return isMobileWindow;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default useMobileResponsive;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { Form, Icon } from '@openedx/paragon';
|
|
2
|
+
import { ExpandMore } from '@openedx/paragon/icons';
|
|
3
|
+
import PropTypes from 'prop-types';
|
|
4
|
+
|
|
5
|
+
const FormFieldRenderer = (props) => {
|
|
6
|
+
let formField = null;
|
|
7
|
+
const {
|
|
8
|
+
className, errorMessage, fieldData, onChangeHandler, isRequired, value,
|
|
9
|
+
} = props;
|
|
10
|
+
|
|
11
|
+
const handleFocus = (e) => {
|
|
12
|
+
if (props.handleFocus) { props.handleFocus(e); }
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const handleOnBlur = (e) => {
|
|
16
|
+
if (props.handleBlur) { props.handleBlur(e); }
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
switch (fieldData.type) {
|
|
20
|
+
case 'select': {
|
|
21
|
+
if (!fieldData.options) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
formField = (
|
|
25
|
+
<Form.Group controlId={fieldData.name} isInvalid={!!(isRequired && errorMessage)}>
|
|
26
|
+
<Form.Control
|
|
27
|
+
className={className}
|
|
28
|
+
as="select"
|
|
29
|
+
name={fieldData.name}
|
|
30
|
+
value={value}
|
|
31
|
+
aria-invalid={isRequired && Boolean(errorMessage)}
|
|
32
|
+
onChange={(e) => onChangeHandler(e)}
|
|
33
|
+
trailingElement={<Icon src={ExpandMore} />}
|
|
34
|
+
floatingLabel={fieldData.label}
|
|
35
|
+
onBlur={handleOnBlur}
|
|
36
|
+
onFocus={handleFocus}
|
|
37
|
+
>
|
|
38
|
+
<option key="default" value="">{fieldData.label}</option>
|
|
39
|
+
{fieldData.options.map(option => (
|
|
40
|
+
<option className="data-hj-suppress" key={option[0]} value={option[0]}>{option[1]}</option>
|
|
41
|
+
))}
|
|
42
|
+
</Form.Control>
|
|
43
|
+
{isRequired && errorMessage && (
|
|
44
|
+
<Form.Control.Feedback id={`${fieldData.name}-error`} type="invalid" className="form-text-size" hasIcon={false}>
|
|
45
|
+
{errorMessage}
|
|
46
|
+
</Form.Control.Feedback>
|
|
47
|
+
)}
|
|
48
|
+
</Form.Group>
|
|
49
|
+
);
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
case 'textarea': {
|
|
53
|
+
formField = (
|
|
54
|
+
<Form.Group controlId={fieldData.name} isInvalid={!!(isRequired && errorMessage)}>
|
|
55
|
+
<Form.Control
|
|
56
|
+
className={className}
|
|
57
|
+
as="textarea"
|
|
58
|
+
name={fieldData.name}
|
|
59
|
+
value={value}
|
|
60
|
+
aria-invalid={isRequired && Boolean(errorMessage)}
|
|
61
|
+
onChange={(e) => onChangeHandler(e)}
|
|
62
|
+
floatingLabel={fieldData.label}
|
|
63
|
+
onBlur={handleOnBlur}
|
|
64
|
+
onFocus={handleFocus}
|
|
65
|
+
/>
|
|
66
|
+
{isRequired && errorMessage && (
|
|
67
|
+
<Form.Control.Feedback id={`${fieldData.name}-error`} type="invalid" className="form-text-size" hasIcon={false}>
|
|
68
|
+
{errorMessage}
|
|
69
|
+
</Form.Control.Feedback>
|
|
70
|
+
)}
|
|
71
|
+
</Form.Group>
|
|
72
|
+
);
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
case 'text': {
|
|
76
|
+
formField = (
|
|
77
|
+
<Form.Group controlId={fieldData.name} isInvalid={!!(isRequired && errorMessage)}>
|
|
78
|
+
<Form.Control
|
|
79
|
+
className={className}
|
|
80
|
+
name={fieldData.name}
|
|
81
|
+
value={value}
|
|
82
|
+
aria-invalid={isRequired && Boolean(errorMessage)}
|
|
83
|
+
onChange={(e) => onChangeHandler(e)}
|
|
84
|
+
floatingLabel={fieldData.label}
|
|
85
|
+
onBlur={handleOnBlur}
|
|
86
|
+
onFocus={handleFocus}
|
|
87
|
+
/>
|
|
88
|
+
{isRequired && errorMessage && (
|
|
89
|
+
<Form.Control.Feedback id={`${fieldData.name}-error`} type="invalid" className="form-text-size" hasIcon={false}>
|
|
90
|
+
{errorMessage}
|
|
91
|
+
</Form.Control.Feedback>
|
|
92
|
+
)}
|
|
93
|
+
</Form.Group>
|
|
94
|
+
);
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
case 'checkbox': {
|
|
98
|
+
formField = (
|
|
99
|
+
<Form.Group isInvalid={!!(isRequired && errorMessage)}>
|
|
100
|
+
<Form.Checkbox
|
|
101
|
+
className={className}
|
|
102
|
+
id={fieldData.name}
|
|
103
|
+
checked={!!value}
|
|
104
|
+
name={fieldData.name}
|
|
105
|
+
value={value}
|
|
106
|
+
aria-invalid={isRequired && Boolean(errorMessage)}
|
|
107
|
+
onChange={(e) => onChangeHandler(e)}
|
|
108
|
+
onBlur={handleOnBlur}
|
|
109
|
+
onFocus={handleFocus}
|
|
110
|
+
>
|
|
111
|
+
{fieldData.label}
|
|
112
|
+
</Form.Checkbox>
|
|
113
|
+
{isRequired && errorMessage && (
|
|
114
|
+
<Form.Control.Feedback id={`${fieldData.name}-error`} type="invalid" className="form-text-size" hasIcon={false}>
|
|
115
|
+
{errorMessage}
|
|
116
|
+
</Form.Control.Feedback>
|
|
117
|
+
)}
|
|
118
|
+
</Form.Group>
|
|
119
|
+
);
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
default:
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return formField;
|
|
127
|
+
};
|
|
128
|
+
FormFieldRenderer.defaultProps = {
|
|
129
|
+
className: '',
|
|
130
|
+
value: '',
|
|
131
|
+
handleBlur: null,
|
|
132
|
+
handleFocus: null,
|
|
133
|
+
errorMessage: '',
|
|
134
|
+
isRequired: false,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
FormFieldRenderer.propTypes = {
|
|
138
|
+
className: PropTypes.string,
|
|
139
|
+
fieldData: PropTypes.shape({
|
|
140
|
+
type: PropTypes.string,
|
|
141
|
+
label: PropTypes.string,
|
|
142
|
+
name: PropTypes.string,
|
|
143
|
+
options: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)),
|
|
144
|
+
}).isRequired,
|
|
145
|
+
onChangeHandler: PropTypes.func.isRequired,
|
|
146
|
+
handleBlur: PropTypes.func,
|
|
147
|
+
handleFocus: PropTypes.func,
|
|
148
|
+
errorMessage: PropTypes.string,
|
|
149
|
+
isRequired: PropTypes.bool,
|
|
150
|
+
value: PropTypes.oneOfType([
|
|
151
|
+
PropTypes.string,
|
|
152
|
+
PropTypes.bool,
|
|
153
|
+
]),
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export default FormFieldRenderer;
|