@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,126 @@
|
|
|
1
|
+
import { distance } from 'fastest-levenshtein';
|
|
2
|
+
|
|
3
|
+
import { VALID_EMAIL_REGEX } from '../../../data/constants';
|
|
4
|
+
import messages from '../../messages';
|
|
5
|
+
import {
|
|
6
|
+
COMMON_EMAIL_PROVIDERS,
|
|
7
|
+
DEFAULT_SERVICE_PROVIDER_DOMAINS,
|
|
8
|
+
DEFAULT_TOP_LEVEL_DOMAINS,
|
|
9
|
+
} from './constants';
|
|
10
|
+
|
|
11
|
+
export const emailRegex = new RegExp(VALID_EMAIL_REGEX, 'i');
|
|
12
|
+
|
|
13
|
+
export const getLevenshteinSuggestion = (word, knownWords, similarityThreshold = 4) => {
|
|
14
|
+
if (!word) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let minEditDistance = 100;
|
|
19
|
+
let mostSimilar = word;
|
|
20
|
+
|
|
21
|
+
for (const knownWord of knownWords) {
|
|
22
|
+
const editDistance = distance(knownWord.toLowerCase(), word.toLowerCase());
|
|
23
|
+
if (editDistance < minEditDistance) {
|
|
24
|
+
minEditDistance = editDistance;
|
|
25
|
+
mostSimilar = knownWord;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return minEditDistance <= similarityThreshold && word !== mostSimilar ? mostSimilar : null;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const getSuggestionForInvalidEmail = (domain, username) => {
|
|
33
|
+
if (!domain) {
|
|
34
|
+
return '';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const defaultDomains = ['yahoo', 'aol', 'hotmail', 'live', 'outlook', 'gmail'];
|
|
38
|
+
const suggestion = getLevenshteinSuggestion(domain, COMMON_EMAIL_PROVIDERS);
|
|
39
|
+
|
|
40
|
+
if (suggestion) {
|
|
41
|
+
return `${username}@${suggestion}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const defaultDomain of defaultDomains) {
|
|
45
|
+
if (domain.includes(defaultDomain)) {
|
|
46
|
+
return `${username}@${defaultDomain}.com`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return '';
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const validateEmailAddress = (value, username, domainName) => {
|
|
54
|
+
let suggestion = null;
|
|
55
|
+
const validation = {
|
|
56
|
+
hasError: false,
|
|
57
|
+
suggestion: '',
|
|
58
|
+
type: '',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const hasMultipleSubdomains = value.match(/\./g).length > 1;
|
|
62
|
+
const [serviceLevelDomain, topLevelDomain] = domainName.split('.');
|
|
63
|
+
const tldSuggestion = !DEFAULT_TOP_LEVEL_DOMAINS.includes(topLevelDomain);
|
|
64
|
+
const serviceSuggestion = getLevenshteinSuggestion(serviceLevelDomain, DEFAULT_SERVICE_PROVIDER_DOMAINS, 2);
|
|
65
|
+
|
|
66
|
+
if (DEFAULT_SERVICE_PROVIDER_DOMAINS.includes(serviceSuggestion || serviceLevelDomain)) {
|
|
67
|
+
suggestion = `${username}@${serviceSuggestion || serviceLevelDomain}.com`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!hasMultipleSubdomains && tldSuggestion) {
|
|
71
|
+
validation.suggestion = suggestion;
|
|
72
|
+
validation.type = 'error';
|
|
73
|
+
} else if (serviceSuggestion) {
|
|
74
|
+
validation.suggestion = suggestion;
|
|
75
|
+
validation.type = 'warning';
|
|
76
|
+
} else {
|
|
77
|
+
suggestion = getLevenshteinSuggestion(domainName, COMMON_EMAIL_PROVIDERS, 3);
|
|
78
|
+
if (suggestion) {
|
|
79
|
+
validation.suggestion = `${username}@${suggestion}`;
|
|
80
|
+
validation.type = 'warning';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!hasMultipleSubdomains && tldSuggestion) {
|
|
85
|
+
validation.hasError = true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return validation;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const validateEmail = (value, confirmEmailValue, formatMessage) => {
|
|
92
|
+
let fieldError = '';
|
|
93
|
+
let confirmEmailError = '';
|
|
94
|
+
let emailSuggestion = { suggestion: '', type: '' };
|
|
95
|
+
|
|
96
|
+
if (!value) {
|
|
97
|
+
fieldError = formatMessage(messages['empty.email.field.error']);
|
|
98
|
+
} else if (value.length <= 2) {
|
|
99
|
+
fieldError = formatMessage(messages['email.invalid.format.error']);
|
|
100
|
+
} else {
|
|
101
|
+
const [username, domainName] = value.split('@');
|
|
102
|
+
// Check if email address is invalid. If we have a suggestion for invalid email
|
|
103
|
+
// provide that along with the error message.
|
|
104
|
+
if (!emailRegex.test(value)) {
|
|
105
|
+
fieldError = formatMessage(messages['email.invalid.format.error']);
|
|
106
|
+
emailSuggestion = {
|
|
107
|
+
suggestion: getSuggestionForInvalidEmail(domainName, username),
|
|
108
|
+
type: 'error',
|
|
109
|
+
};
|
|
110
|
+
} else {
|
|
111
|
+
const response = validateEmailAddress(value, username, domainName);
|
|
112
|
+
if (response.hasError) {
|
|
113
|
+
fieldError = formatMessage(messages['email.invalid.format.error']);
|
|
114
|
+
delete response.hasError;
|
|
115
|
+
}
|
|
116
|
+
emailSuggestion = { ...response };
|
|
117
|
+
|
|
118
|
+
if (confirmEmailValue && value !== confirmEmailValue) {
|
|
119
|
+
confirmEmailError = formatMessage(messages['email.do.not.match']);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return { fieldError, confirmEmailError, suggestion: emailSuggestion };
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export default validateEmail;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { COMMON_EMAIL_PROVIDERS } from './constants';
|
|
2
|
+
import {
|
|
3
|
+
getLevenshteinSuggestion,
|
|
4
|
+
getSuggestionForInvalidEmail,
|
|
5
|
+
} from './validator';
|
|
6
|
+
|
|
7
|
+
describe('Email Validators Utils', () => {
|
|
8
|
+
describe('getLevenshteinSuggestion Tests', () => {
|
|
9
|
+
it('test getLevenshteinSuggestion returns null for invalid word', () => {
|
|
10
|
+
const output = getLevenshteinSuggestion('', COMMON_EMAIL_PROVIDERS);
|
|
11
|
+
expect(output).toEqual(null);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('test getLevenshteinSuggestion returns output for valid word', () => {
|
|
15
|
+
const output = getLevenshteinSuggestion('gmail', COMMON_EMAIL_PROVIDERS);
|
|
16
|
+
expect(output).toEqual('gmail.com');
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
describe('getSuggestionForInvalidEmail Tests', () => {
|
|
20
|
+
it('test getSuggestionForInvalidEmail returns empty string for invalid domain', () => {
|
|
21
|
+
const output = getSuggestionForInvalidEmail('', 'username');
|
|
22
|
+
expect(output).toEqual('');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('test getSuggestionForInvalidEmail returns valid suggestion when domain is nearly matched', () => {
|
|
26
|
+
const output = getSuggestionForInvalidEmail('gmail', 'username');
|
|
27
|
+
expect(output).toEqual('username@gmail.com');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('test getSuggestionForInvalidEmail returns valid suggestion for default domains', () => {
|
|
31
|
+
const output = getSuggestionForInvalidEmail('aol', 'username');
|
|
32
|
+
expect(output).toEqual('username@aol.com');
|
|
33
|
+
});
|
|
34
|
+
it('test getSuggestionForInvalidEmail returns empty for totally different domain', () => {
|
|
35
|
+
const output = getSuggestionForInvalidEmail('invalid-domain', 'username');
|
|
36
|
+
expect(output).toEqual('');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
import { FormattedMessage, useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
|
|
4
|
+
import { Form, Hyperlink } from '@openedx/paragon';
|
|
5
|
+
import PropTypes from 'prop-types';
|
|
6
|
+
|
|
7
|
+
import messages from '../../messages';
|
|
8
|
+
|
|
9
|
+
const HonorCode = (props) => {
|
|
10
|
+
const { formatMessage } = useIntl();
|
|
11
|
+
const {
|
|
12
|
+
errorMessage, onChangeHandler, fieldType, value,
|
|
13
|
+
} = props;
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (fieldType === 'tos_and_honor_code' && !value) {
|
|
17
|
+
onChangeHandler({ target: { name: 'honor_code', value: true } });
|
|
18
|
+
}
|
|
19
|
+
}, [fieldType, onChangeHandler, value]);
|
|
20
|
+
|
|
21
|
+
if (fieldType === 'tos_and_honor_code') {
|
|
22
|
+
return (
|
|
23
|
+
<div id="honor-code" className="micro text-muted mt-4">
|
|
24
|
+
<FormattedMessage
|
|
25
|
+
id="register.page.terms.of.service.and.honor.code"
|
|
26
|
+
defaultMessage="By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each
|
|
27
|
+
Member process your personal data in accordance with the {privacyPolicy}."
|
|
28
|
+
description="Text that appears on registration form stating honor code and privacy policy"
|
|
29
|
+
values={{
|
|
30
|
+
platformName: getSiteConfig().siteName,
|
|
31
|
+
tosAndHonorCode: (
|
|
32
|
+
<Hyperlink
|
|
33
|
+
className="inline-link"
|
|
34
|
+
destination={useAppConfig().TOS_AND_HONOR_CODE || '#'}
|
|
35
|
+
target="_blank"
|
|
36
|
+
showLaunchIcon={false}
|
|
37
|
+
>
|
|
38
|
+
{formatMessage(messages['terms.of.service.and.honor.code'])}
|
|
39
|
+
</Hyperlink>
|
|
40
|
+
),
|
|
41
|
+
privacyPolicy: (
|
|
42
|
+
<Hyperlink
|
|
43
|
+
className="inline-link"
|
|
44
|
+
destination={useAppConfig().PRIVACY_POLICY || '#'}
|
|
45
|
+
target="_blank"
|
|
46
|
+
showLaunchIcon={false}
|
|
47
|
+
>
|
|
48
|
+
{formatMessage(messages['privacy.policy'])}
|
|
49
|
+
</Hyperlink>
|
|
50
|
+
),
|
|
51
|
+
}}
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div id="honor-code" className="micro text-muted">
|
|
59
|
+
<Form.Checkbox
|
|
60
|
+
className="form-field--checkbox mt-1"
|
|
61
|
+
id="honor-code"
|
|
62
|
+
checked={value}
|
|
63
|
+
name="honor_code"
|
|
64
|
+
value={value}
|
|
65
|
+
onChange={onChangeHandler}
|
|
66
|
+
>
|
|
67
|
+
<FormattedMessage
|
|
68
|
+
id="register.page.honor.code"
|
|
69
|
+
defaultMessage="I agree to the {platformName} {tosAndHonorCode}"
|
|
70
|
+
description="Text that appears on registration form stating honor code"
|
|
71
|
+
values={{
|
|
72
|
+
platformName: getSiteConfig().siteName,
|
|
73
|
+
tosAndHonorCode: (
|
|
74
|
+
<Hyperlink variant="muted" destination={useAppConfig().TOS_AND_HONOR_CODE || '#'} target="_blank">
|
|
75
|
+
{formatMessage(messages['honor.code'])}
|
|
76
|
+
</Hyperlink>
|
|
77
|
+
),
|
|
78
|
+
}}
|
|
79
|
+
/>
|
|
80
|
+
</Form.Checkbox>
|
|
81
|
+
{errorMessage && (
|
|
82
|
+
<Form.Control.Feedback type="invalid" className="form-text-size" hasIcon={false}>
|
|
83
|
+
{errorMessage}
|
|
84
|
+
</Form.Control.Feedback>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
HonorCode.defaultProps = {
|
|
91
|
+
errorMessage: '',
|
|
92
|
+
onChangeHandler: null,
|
|
93
|
+
fieldType: 'honor_code',
|
|
94
|
+
value: false,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
HonorCode.propTypes = {
|
|
98
|
+
errorMessage: PropTypes.string,
|
|
99
|
+
onChangeHandler: PropTypes.func,
|
|
100
|
+
fieldType: PropTypes.string,
|
|
101
|
+
value: PropTypes.bool,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export default HonorCode;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CurrentAppProvider, getSiteConfig, injectIntl, IntlProvider, mergeAppConfig
|
|
3
|
+
} from '@openedx/frontend-base';
|
|
4
|
+
import { render } from '@testing-library/react';
|
|
5
|
+
|
|
6
|
+
import { appId } from '../../../constants';
|
|
7
|
+
import { HonorCode } from '../index';
|
|
8
|
+
|
|
9
|
+
const IntlHonorCode = injectIntl(HonorCode);
|
|
10
|
+
|
|
11
|
+
const providerWrapper = children => (
|
|
12
|
+
<IntlProvider locale="en">
|
|
13
|
+
<CurrentAppProvider appId={appId}>
|
|
14
|
+
{children}
|
|
15
|
+
</CurrentAppProvider>
|
|
16
|
+
</IntlProvider>
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
describe('HonorCodeTest', () => {
|
|
20
|
+
mergeAppConfig(appId, {
|
|
21
|
+
PRIVACY_POLICY: 'http://privacy-policy.com',
|
|
22
|
+
TOS_AND_HONOR_CODE: 'http://tos-and-honot-code.com',
|
|
23
|
+
});
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
25
|
+
let value = false;
|
|
26
|
+
|
|
27
|
+
const changeHandler = (e) => {
|
|
28
|
+
value = e.target.checked;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
value = false;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should render error msg if honor code is not checked', () => {
|
|
36
|
+
const errorMessage = `You must agree to the ${getSiteConfig().siteName} Honor Code`;
|
|
37
|
+
const { container } = render(providerWrapper(
|
|
38
|
+
<IntlHonorCode
|
|
39
|
+
errorMessage={errorMessage}
|
|
40
|
+
onChangeHandler={changeHandler}
|
|
41
|
+
/>
|
|
42
|
+
));
|
|
43
|
+
const errorElement = container.querySelector('.form-text-size'); // Adjust the selector as per your component
|
|
44
|
+
|
|
45
|
+
expect(errorElement.textContent).toEqual(errorMessage);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should render Honor code field', () => {
|
|
49
|
+
const expectedMsg = `I agree to the ${getSiteConfig().siteName}\u00a0Honor Codein a new tab`;
|
|
50
|
+
const { container } = render(providerWrapper(
|
|
51
|
+
<IntlHonorCode onChangeHandler={changeHandler} />
|
|
52
|
+
));
|
|
53
|
+
|
|
54
|
+
const honorCodeField = container.querySelector('#honor-code');
|
|
55
|
+
honorCodeField.dispatchEvent(new MouseEvent('change', { bubbles: true }));
|
|
56
|
+
|
|
57
|
+
expect(honorCodeField.querySelector('label').textContent).toEqual(expectedMsg);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should render Terms of Service and Honor code field', () => {
|
|
61
|
+
const { container } = render(providerWrapper(
|
|
62
|
+
<IntlHonorCode fieldType="tos_and_honor_code" onChangeHandler={changeHandler} />
|
|
63
|
+
));
|
|
64
|
+
const expectedMsg = 'By creating an account, you agree to the Terms of Service and Honor Code and you '
|
|
65
|
+
+ `acknowledge that ${getSiteConfig().siteName} and each Member process your personal data in `
|
|
66
|
+
+ 'accordance with the Privacy Policy.';
|
|
67
|
+
const honorCodeField = container.querySelector('#honor-code');
|
|
68
|
+
expect(honorCodeField.textContent).toEqual(expectedMsg);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { useDispatch, useSelector } from 'react-redux';
|
|
2
|
+
|
|
3
|
+
import { useIntl } from '@openedx/frontend-base';
|
|
4
|
+
import PropTypes from 'prop-types';
|
|
5
|
+
|
|
6
|
+
import { FormGroup } from '../../../common-components';
|
|
7
|
+
import { clearRegistrationBackendError, fetchRealtimeValidations } from '../../data/actions';
|
|
8
|
+
import validateName from './validator';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Name field wrapper. It accepts following handlers
|
|
12
|
+
* - handleChange for setting value change and
|
|
13
|
+
* - handleErrorChange for setting error
|
|
14
|
+
*
|
|
15
|
+
* It is responsible for
|
|
16
|
+
* - Making backend call for generating username suggestions
|
|
17
|
+
* - Performing name field validations
|
|
18
|
+
* - Clearing error on focus
|
|
19
|
+
* - Setting value on change
|
|
20
|
+
*/
|
|
21
|
+
const NameField = (props) => {
|
|
22
|
+
const { formatMessage } = useIntl();
|
|
23
|
+
const dispatch = useDispatch();
|
|
24
|
+
const validationApiRateLimited = useSelector(state => state.register.validationApiRateLimited);
|
|
25
|
+
|
|
26
|
+
const {
|
|
27
|
+
handleErrorChange,
|
|
28
|
+
shouldFetchUsernameSuggestions,
|
|
29
|
+
} = props;
|
|
30
|
+
|
|
31
|
+
const handleOnBlur = (e) => {
|
|
32
|
+
const { value } = e.target;
|
|
33
|
+
const fieldError = validateName(value, formatMessage);
|
|
34
|
+
if (fieldError) {
|
|
35
|
+
handleErrorChange('name', fieldError);
|
|
36
|
+
} else if (shouldFetchUsernameSuggestions && !validationApiRateLimited) {
|
|
37
|
+
dispatch(fetchRealtimeValidations({ name: value }));
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const handleOnFocus = () => {
|
|
42
|
+
handleErrorChange('name', '');
|
|
43
|
+
dispatch(clearRegistrationBackendError('name'));
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<FormGroup
|
|
48
|
+
{...props}
|
|
49
|
+
handleBlur={handleOnBlur}
|
|
50
|
+
handleFocus={handleOnFocus}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
NameField.defaultProps = {
|
|
56
|
+
errorMessage: '',
|
|
57
|
+
shouldFetchUsernameSuggestions: false,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
NameField.propTypes = {
|
|
61
|
+
errorMessage: PropTypes.string,
|
|
62
|
+
shouldFetchUsernameSuggestions: PropTypes.bool,
|
|
63
|
+
value: PropTypes.string.isRequired,
|
|
64
|
+
handleChange: PropTypes.func.isRequired,
|
|
65
|
+
handleErrorChange: PropTypes.func.isRequired,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export default NameField;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { Provider } from 'react-redux';
|
|
2
|
+
|
|
3
|
+
import { injectIntl, IntlProvider } from '@openedx/frontend-base';
|
|
4
|
+
import { fireEvent, render } from '@testing-library/react';
|
|
5
|
+
import { BrowserRouter as Router } from 'react-router-dom';
|
|
6
|
+
import configureStore from 'redux-mock-store';
|
|
7
|
+
|
|
8
|
+
import { clearRegistrationBackendError, fetchRealtimeValidations } from '../../data/actions';
|
|
9
|
+
import { NameField } from '../index';
|
|
10
|
+
|
|
11
|
+
const IntlNameField = injectIntl(NameField);
|
|
12
|
+
const mockStore = configureStore();
|
|
13
|
+
|
|
14
|
+
jest.mock('react-router-dom', () => {
|
|
15
|
+
const mockNavigation = jest.fn();
|
|
16
|
+
|
|
17
|
+
// eslint-disable-next-line react/prop-types
|
|
18
|
+
const Navigate = ({ to }) => {
|
|
19
|
+
mockNavigation(to);
|
|
20
|
+
return <div />;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
...jest.requireActual('react-router-dom'),
|
|
25
|
+
Navigate,
|
|
26
|
+
mockNavigate: mockNavigation,
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('NameField', () => {
|
|
31
|
+
let props = {};
|
|
32
|
+
let store = {};
|
|
33
|
+
|
|
34
|
+
const reduxWrapper = children => (
|
|
35
|
+
<IntlProvider locale="en">
|
|
36
|
+
<Provider store={store}>{children}</Provider>
|
|
37
|
+
</IntlProvider>
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const routerWrapper = children => (
|
|
41
|
+
<Router>
|
|
42
|
+
{children}
|
|
43
|
+
</Router>
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const initialState = {
|
|
47
|
+
register: {},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
store = mockStore(initialState);
|
|
52
|
+
props = {
|
|
53
|
+
name: 'name',
|
|
54
|
+
value: '',
|
|
55
|
+
errorMessage: '',
|
|
56
|
+
handleChange: jest.fn(),
|
|
57
|
+
handleErrorChange: jest.fn(),
|
|
58
|
+
floatingLabel: '',
|
|
59
|
+
};
|
|
60
|
+
window.location = { search: '' };
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
afterEach(() => {
|
|
64
|
+
jest.clearAllMocks();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('Test Name Field', () => {
|
|
68
|
+
const fieldValidation = { name: 'Enter your full name' };
|
|
69
|
+
|
|
70
|
+
it('should run name field validation when onBlur is fired', () => {
|
|
71
|
+
const { container } = render(routerWrapper(reduxWrapper(<IntlNameField {...props} />)));
|
|
72
|
+
|
|
73
|
+
const nameInput = container.querySelector('input#name');
|
|
74
|
+
fireEvent.blur(nameInput, { target: { value: '', name: 'name' } });
|
|
75
|
+
|
|
76
|
+
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
|
|
77
|
+
expect(props.handleErrorChange).toHaveBeenCalledWith(
|
|
78
|
+
'name',
|
|
79
|
+
fieldValidation.name,
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should update errors for frontend validations', () => {
|
|
84
|
+
const { container } = render(routerWrapper(reduxWrapper(<IntlNameField {...props} />)));
|
|
85
|
+
|
|
86
|
+
const nameInput = container.querySelector('input#name');
|
|
87
|
+
fireEvent.blur(nameInput, { target: { value: 'https://invalid-name.com', name: 'name' } });
|
|
88
|
+
|
|
89
|
+
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
|
|
90
|
+
expect(props.handleErrorChange).toHaveBeenCalledWith(
|
|
91
|
+
'name',
|
|
92
|
+
'Enter a valid name',
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should clear error on focus', () => {
|
|
97
|
+
const { container } = render(routerWrapper(reduxWrapper(<IntlNameField {...props} />)));
|
|
98
|
+
|
|
99
|
+
const nameInput = container.querySelector('input#name');
|
|
100
|
+
fireEvent.focus(nameInput, { target: { value: '', name: 'name' } });
|
|
101
|
+
|
|
102
|
+
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
|
|
103
|
+
expect(props.handleErrorChange).toHaveBeenCalledWith(
|
|
104
|
+
'name',
|
|
105
|
+
'',
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should call backend validation api on blur event, if frontend validations have passed', () => {
|
|
110
|
+
store.dispatch = jest.fn(store.dispatch);
|
|
111
|
+
props = {
|
|
112
|
+
...props,
|
|
113
|
+
shouldFetchUsernameSuggestions: true,
|
|
114
|
+
};
|
|
115
|
+
const { container } = render(routerWrapper(reduxWrapper(<IntlNameField {...props} />)));
|
|
116
|
+
|
|
117
|
+
const nameInput = container.querySelector('input#name');
|
|
118
|
+
// Enter a valid name so that frontend validations are passed
|
|
119
|
+
fireEvent.blur(nameInput, { target: { value: 'test', name: 'name' } });
|
|
120
|
+
|
|
121
|
+
expect(store.dispatch).toHaveBeenCalledWith(fetchRealtimeValidations({ name: 'test' }));
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should clear the registration validation error on focus on field', () => {
|
|
125
|
+
const nameError = 'temp error';
|
|
126
|
+
store = mockStore({
|
|
127
|
+
...initialState,
|
|
128
|
+
register: {
|
|
129
|
+
...initialState.register,
|
|
130
|
+
registrationError: {
|
|
131
|
+
name: [{ userMessage: nameError }],
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
store.dispatch = jest.fn(store.dispatch);
|
|
137
|
+
const { container } = render(routerWrapper(reduxWrapper(<IntlNameField {...props} />)));
|
|
138
|
+
|
|
139
|
+
const nameInput = container.querySelector('input#name');
|
|
140
|
+
|
|
141
|
+
fireEvent.focus(nameInput, { target: { value: 'test', name: 'name' } });
|
|
142
|
+
|
|
143
|
+
expect(store.dispatch).toHaveBeenCalledWith(clearRegistrationBackendError('name'));
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import messages from '../../messages';
|
|
2
|
+
|
|
3
|
+
// regex more focused towards url matching
|
|
4
|
+
export const URL_REGEX = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?/gi; // eslint-disable-line no-useless-escape
|
|
5
|
+
|
|
6
|
+
// regex for html tags
|
|
7
|
+
export const HTML_REGEX = /<|>/u;
|
|
8
|
+
|
|
9
|
+
// regex from backend
|
|
10
|
+
export const INVALID_NAME_REGEX = /https?:\/\/(?:[-\w.]|(?:%[\da-fA-F]{2}))*/g;
|
|
11
|
+
|
|
12
|
+
const validateName = (value, formatMessage) => {
|
|
13
|
+
let fieldError = '';
|
|
14
|
+
if (!value.trim()) {
|
|
15
|
+
fieldError = formatMessage(messages['empty.name.field.error']);
|
|
16
|
+
} else if (URL_REGEX.test(value) || HTML_REGEX.test(value) || INVALID_NAME_REGEX.test(value)) {
|
|
17
|
+
fieldError = formatMessage(messages['name.validation.message']);
|
|
18
|
+
}
|
|
19
|
+
return fieldError;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default validateName;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { FormattedMessage, useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
|
|
2
|
+
import { Form, Hyperlink } from '@openedx/paragon';
|
|
3
|
+
import PropTypes from 'prop-types';
|
|
4
|
+
|
|
5
|
+
import messages from '../../messages';
|
|
6
|
+
|
|
7
|
+
const TermsOfService = (props) => {
|
|
8
|
+
const { formatMessage } = useIntl();
|
|
9
|
+
const {
|
|
10
|
+
errorMessage, onChangeHandler, value,
|
|
11
|
+
} = props;
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div id="terms-of-service" className="micro text-muted">
|
|
15
|
+
<Form.Checkbox
|
|
16
|
+
className="form-field--checkbox mt-1"
|
|
17
|
+
id="tos"
|
|
18
|
+
checked={value}
|
|
19
|
+
name="terms_of_service"
|
|
20
|
+
value={value}
|
|
21
|
+
onChange={onChangeHandler}
|
|
22
|
+
>
|
|
23
|
+
<FormattedMessage
|
|
24
|
+
id="register.page.terms.of.service"
|
|
25
|
+
defaultMessage="I agree to the {platformName} {termsOfService}"
|
|
26
|
+
description="Text that appears on registration form stating terms of service.
|
|
27
|
+
It is a legal document that users must agree to."
|
|
28
|
+
values={{
|
|
29
|
+
platformName: getSiteConfig().siteName,
|
|
30
|
+
termsOfService: (
|
|
31
|
+
<Hyperlink variant="muted" destination={useAppConfig().TOS_LINK || '#'} target="_blank">
|
|
32
|
+
{formatMessage(messages['terms.of.service'])}
|
|
33
|
+
</Hyperlink>
|
|
34
|
+
),
|
|
35
|
+
}}
|
|
36
|
+
/>
|
|
37
|
+
</Form.Checkbox>
|
|
38
|
+
{errorMessage && (
|
|
39
|
+
<Form.Control.Feedback type="invalid" className="form-text-size" hasIcon={false}>
|
|
40
|
+
{errorMessage}
|
|
41
|
+
</Form.Control.Feedback>
|
|
42
|
+
)}
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
TermsOfService.defaultProps = {
|
|
48
|
+
errorMessage: '',
|
|
49
|
+
value: false,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
TermsOfService.propTypes = {
|
|
53
|
+
errorMessage: PropTypes.string,
|
|
54
|
+
onChangeHandler: PropTypes.func.isRequired,
|
|
55
|
+
value: PropTypes.bool,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export default TermsOfService;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { getSiteConfig, injectIntl, IntlProvider } from '@openedx/frontend-base';
|
|
2
|
+
import { fireEvent, render } from '@testing-library/react';
|
|
3
|
+
|
|
4
|
+
import { TermsOfService } from '../index';
|
|
5
|
+
|
|
6
|
+
const IntlTermsOfService = injectIntl(TermsOfService);
|
|
7
|
+
|
|
8
|
+
describe('TermsOfServiceTest', () => {
|
|
9
|
+
let value = false;
|
|
10
|
+
|
|
11
|
+
const changeHandler = (e) => {
|
|
12
|
+
value = e.target.checked;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
value = false;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should render error msg if Terms of Service checkbox is not checked', () => {
|
|
20
|
+
const errorMessage = `You must agree to the ${getSiteConfig().siteName} Terms of Service`;
|
|
21
|
+
const { container } = render(
|
|
22
|
+
<IntlProvider locale="en">
|
|
23
|
+
<IntlTermsOfService errorMessage={errorMessage} onChangeHandler={changeHandler} />
|
|
24
|
+
</IntlProvider>,
|
|
25
|
+
);
|
|
26
|
+
const errorElement = container.querySelector('.form-text-size');
|
|
27
|
+
expect(errorElement.textContent).toEqual(errorMessage);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should render Terms of Service field', () => {
|
|
31
|
+
const { container } = render(
|
|
32
|
+
<IntlProvider locale="en">
|
|
33
|
+
<IntlTermsOfService onChangeHandler={changeHandler} />
|
|
34
|
+
</IntlProvider>,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const expectedMsg = `I agree to the ${getSiteConfig().siteName}\u00a0Terms of Servicein a new tab`;
|
|
38
|
+
|
|
39
|
+
const termsOfServiceLabel = container.querySelector('#terms-of-service label');
|
|
40
|
+
expect(termsOfServiceLabel.textContent).toEqual(expectedMsg);
|
|
41
|
+
|
|
42
|
+
expect(value).toEqual(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should change value when Terms of Service field is checked', () => {
|
|
46
|
+
const { container } = render(
|
|
47
|
+
<IntlProvider locale="en">
|
|
48
|
+
<IntlTermsOfService onChangeHandler={changeHandler} />
|
|
49
|
+
</IntlProvider>,
|
|
50
|
+
);
|
|
51
|
+
const field = container.querySelector('input#tos');
|
|
52
|
+
fireEvent.click(field);
|
|
53
|
+
expect(value).toEqual(true);
|
|
54
|
+
});
|
|
55
|
+
});
|