fix: email validation and analytic events (#333)
Fixed email validation logic and clear error state on close. Also fixed analytic events and removed unused components. VAN-538, VAN-568
This commit is contained in:
@@ -20,7 +20,7 @@ SEGMENT_KEY=null
|
||||
SITE_NAME='edX'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
AUTHN_MINIMAL_HEADER=true
|
||||
LOGIN_ISSUE_SUPPORT_LINK=''
|
||||
LOGIN_ISSUE_SUPPORT_LINK='/login-issue-support-url'
|
||||
TOS_AND_HONOR_CODE='http://localhost:18000/honor'
|
||||
PRIVACY_POLICY='http://localhost:18000/privacy'
|
||||
REGISTRATION_OPTIONAL_FIELDS=''
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import { Info } from '@edx/paragon/icons';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function AlertDismissible(props) {
|
||||
const [show, setShow] = useState(true);
|
||||
|
||||
return show ? (
|
||||
<Alert variant={props.variant} onClose={() => setShow(false)} dismissible className="pb-1 pt-1" icon={Info}>
|
||||
<p>{props.msg}</p>
|
||||
</Alert>
|
||||
) : null;
|
||||
}
|
||||
|
||||
AlertDismissible.propTypes = {
|
||||
variant: PropTypes.string.isRequired,
|
||||
msg: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(AlertDismissible);
|
||||
@@ -1,10 +1,9 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import {
|
||||
Form, Hyperlink, TransitionReplace,
|
||||
Form, TransitionReplace,
|
||||
} from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
import AlertDismissible from './AlertDismissible';
|
||||
|
||||
const FormGroup = (props) => {
|
||||
const [hasFocus, setHasFocus] = useState(false);
|
||||
@@ -56,10 +55,6 @@ const FormGroup = (props) => {
|
||||
{props.errorMessage !== '' && (
|
||||
<Form.Control.Feedback key="error" hasIcon={false} feedback-for={props.name} type="invalid">{props.errorMessage}</Form.Control.Feedback>
|
||||
)}
|
||||
|
||||
{props.suggestedTopLevelDomain ? <AlertDismissible msg={props.suggestedTopLevelDomain} variant="danger" /> : null}
|
||||
|
||||
{props.suggestedServiceLevelDomain ? <span className="one-rem-font">{props.suggestedServiceLevelDomain.split(':')[0]}: <Hyperlink destination="#"><u>{props.suggestedServiceLevelDomain.split(':')[1]}</u></Hyperlink></span> : null}
|
||||
{props.children}
|
||||
</Form.Group>
|
||||
);
|
||||
@@ -69,8 +64,6 @@ FormGroup.defaultProps = {
|
||||
as: 'input',
|
||||
errorMessage: '',
|
||||
borderClass: '',
|
||||
suggestedTopLevelDomain: '',
|
||||
suggestedServiceLevelDomain: '',
|
||||
autoComplete: null,
|
||||
handleBlur: null,
|
||||
handleChange: () => {},
|
||||
@@ -87,8 +80,6 @@ FormGroup.propTypes = {
|
||||
as: PropTypes.string,
|
||||
errorMessage: PropTypes.string,
|
||||
borderClass: PropTypes.string,
|
||||
suggestedTopLevelDomain: PropTypes.string,
|
||||
suggestedServiceLevelDomain: PropTypes.string,
|
||||
autoComplete: PropTypes.string,
|
||||
floatingLabel: PropTypes.string.isRequired,
|
||||
handleBlur: PropTypes.func,
|
||||
|
||||
@@ -27,6 +27,11 @@ const Logistration = (props) => {
|
||||
setInstitutionLogin(!institutionLogin);
|
||||
};
|
||||
|
||||
const handleOnSelect = (tabKey) => {
|
||||
sendTrackEvent(`edx.bi.${tabKey.replace('/', '')}_form.toggled`, { category: 'user-engagement' });
|
||||
setKey(tabKey);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{institutionLogin
|
||||
@@ -48,7 +53,7 @@ const Logistration = (props) => {
|
||||
: (
|
||||
<>
|
||||
{!tpa && (
|
||||
<Tabs defaultActiveKey={selectedPage} id="controlled-tab-example" onSelect={(k) => setKey(k)}>
|
||||
<Tabs defaultActiveKey={selectedPage} id="controlled-tab-example" onSelect={handleOnSelect}>
|
||||
<Tab title={intl.formatMessage(messages['logistration.register'])} eventKey={REGISTER_PAGE} />
|
||||
<Tab title={intl.formatMessage(messages['logistration.sign.in'])} eventKey={LOGIN_PAGE} />
|
||||
</Tabs>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
import { Formik } from 'formik';
|
||||
import PropTypes from 'prop-types';
|
||||
@@ -7,7 +7,7 @@ import { Helmet } from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { sendPageEvent } from '@edx/frontend-platform/analytics';
|
||||
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Form, StatefulButton, Hyperlink } from '@edx/paragon';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
@@ -29,6 +29,11 @@ const ForgotPasswordPage = (props) => {
|
||||
const regex = new RegExp(VALID_EMAIL_REGEX, 'i');
|
||||
const [validationError, setValidationError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
sendPageEvent('login_and_registration', 'reset');
|
||||
sendTrackEvent('edx.bi.password_reset_form.viewed', { category: 'user-engagement' });
|
||||
}, []);
|
||||
|
||||
const getValidationMessage = (email) => {
|
||||
let error = '';
|
||||
|
||||
@@ -42,8 +47,6 @@ const ForgotPasswordPage = (props) => {
|
||||
return error;
|
||||
};
|
||||
|
||||
sendPageEvent('login_and_registration', 'reset');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span className="nav nav-tabs">
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faCaretDown, faCaretRight } from '@fortawesome/free-solid-svg-icons';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
|
||||
import SwitchContent from '../common-components/SwitchContent';
|
||||
import {
|
||||
LOGIN_PAGE,
|
||||
REGISTER_PAGE,
|
||||
RESET_PAGE,
|
||||
} from '../data/constants';
|
||||
import messages from './messages';
|
||||
import { updatePathWithQueryParams } from '../data/utils';
|
||||
|
||||
const LoginHelpLinks = (props) => {
|
||||
const { intl, page } = props;
|
||||
const [showLoginHelp, setShowLoginHelpValue] = useState(false);
|
||||
|
||||
const toggleLoginHelp = (e) => {
|
||||
e.preventDefault();
|
||||
setShowLoginHelpValue(!showLoginHelp);
|
||||
};
|
||||
|
||||
const handleForgotPasswordLinkClickEvent = () => {
|
||||
sendTrackEvent('edx.bi.password-reset_form.toggled', { category: 'user-engagement' });
|
||||
};
|
||||
|
||||
const forgotPasswordLink = () => (
|
||||
<Hyperlink
|
||||
className="field-link"
|
||||
destination={updatePathWithQueryParams(RESET_PAGE)}
|
||||
onClick={handleForgotPasswordLinkClickEvent}
|
||||
>
|
||||
{intl.formatMessage(messages['forgot.password.link'])}
|
||||
</Hyperlink>
|
||||
);
|
||||
|
||||
const signUpLink = () => (
|
||||
<Hyperlink className="field-link" destination={updatePathWithQueryParams(REGISTER_PAGE)}>
|
||||
{intl.formatMessage(messages['register.link'])}
|
||||
</Hyperlink>
|
||||
);
|
||||
|
||||
const loginIssueSupportURL = (config) => (config.LOGIN_ISSUE_SUPPORT_LINK
|
||||
? (
|
||||
<Hyperlink className="field-link" destination={config.LOGIN_ISSUE_SUPPORT_LINK}>
|
||||
{intl.formatMessage(messages['other.sign.in.issues'])}
|
||||
</Hyperlink>
|
||||
)
|
||||
: null);
|
||||
|
||||
const getHelpButtonMessage = () => {
|
||||
let mid = 'need.other.help.signing.in.collapsible.menu';
|
||||
if (page === LOGIN_PAGE) {
|
||||
mid = 'need.help.signing.in.collapsible.menu';
|
||||
}
|
||||
|
||||
return intl.formatMessage(messages[mid]);
|
||||
};
|
||||
|
||||
const renderLoginHelp = () => (
|
||||
<div className="login-help small">
|
||||
{ page === LOGIN_PAGE ? forgotPasswordLink() : signUpLink() }
|
||||
{ loginIssueSupportURL(getConfig()) }
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button type="button" className="mt-2 field-link small" onClick={toggleLoginHelp}>
|
||||
<FontAwesomeIcon className="mr-1" icon={showLoginHelp ? faCaretDown : faCaretRight} />
|
||||
{getHelpButtonMessage()}
|
||||
</button>
|
||||
<SwitchContent
|
||||
expression={showLoginHelp ? 'showHelp' : 'default'}
|
||||
cases={{
|
||||
showHelp: renderLoginHelp(),
|
||||
default: <></>,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
LoginHelpLinks.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
page: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(LoginHelpLinks);
|
||||
@@ -7,7 +7,7 @@ import { connect } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { sendPageEvent } from '@edx/frontend-platform/analytics';
|
||||
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Form, Hyperlink, Icon, StatefulButton,
|
||||
@@ -101,6 +101,10 @@ class LoginPage extends React.Component {
|
||||
this.setState({ errors });
|
||||
}
|
||||
|
||||
handleForgotPasswordLinkClickEvent = () => {
|
||||
sendTrackEvent('edx.bi.password-reset_form.toggled', { category: 'user-engagement' });
|
||||
};
|
||||
|
||||
validateEmail(email) {
|
||||
const { errors } = this.state;
|
||||
|
||||
@@ -242,7 +246,12 @@ class LoginPage extends React.Component {
|
||||
onClick={this.handleSubmit}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
/>
|
||||
<Link id="forgot-password" className="btn btn-link font-weight-500 text-body" to={RESET_PAGE}>
|
||||
<Link
|
||||
id="forgot-password"
|
||||
className="btn btn-link font-weight-500 text-body"
|
||||
to={RESET_PAGE}
|
||||
onClick={this.handleForgotPasswordLinkClickEvent}
|
||||
>
|
||||
{intl.formatMessage(messages['forgot.password'])}
|
||||
</Link>
|
||||
{this.renderThirdPartyAuth(providers, secondaryProviders, currentProvider, thirdPartyAuthApiStatus, intl)}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import React from 'react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import * as analytics from '@edx/frontend-platform/analytics';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import LoginHelpLinks from '../LoginHelpLinks';
|
||||
import { LOGIN_PAGE } from '../../data/constants';
|
||||
|
||||
const otherSignInIssues = 'https://login-issue-support-url.com';
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
getConfig: jest.fn().mockReturnValue({ LOGIN_ISSUE_SUPPORT_LINK: otherSignInIssues }),
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/analytics');
|
||||
analytics.sendTrackEvent = jest.fn();
|
||||
|
||||
describe('LoginHelpLinks', () => {
|
||||
let props = {};
|
||||
|
||||
const reduxWrapper = children => (
|
||||
<IntlProvider locale="en">
|
||||
{children}
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
it('renders help links on button click', () => {
|
||||
props = {
|
||||
...props,
|
||||
page: LOGIN_PAGE,
|
||||
};
|
||||
const loginHelpLinks = mount(reduxWrapper(<LoginHelpLinks {...props} />));
|
||||
|
||||
expect(loginHelpLinks.find('.login-help').length).toBe(0);
|
||||
loginHelpLinks.find('button').first().simulate('click');
|
||||
expect(loginHelpLinks.find('.login-help').length).toBe(1);
|
||||
});
|
||||
|
||||
it('should display login page help links', () => {
|
||||
props = {
|
||||
...props,
|
||||
page: LOGIN_PAGE,
|
||||
};
|
||||
|
||||
const wrapper = mount(reduxWrapper(<LoginHelpLinks {...props} />));
|
||||
wrapper.find('button').first().simulate('click');
|
||||
|
||||
const loginHelpLinks = wrapper.find('a');
|
||||
|
||||
expect(loginHelpLinks.at(0).prop('href')).toEqual('/reset');
|
||||
expect(loginHelpLinks.at(1).prop('href')).toEqual(otherSignInIssues);
|
||||
});
|
||||
|
||||
it('should display forget password page help links', () => {
|
||||
props = {
|
||||
...props,
|
||||
page: 'forget-password',
|
||||
};
|
||||
|
||||
const wrapper = mount(reduxWrapper(<LoginHelpLinks {...props} />));
|
||||
wrapper.find('button').first().simulate('click');
|
||||
|
||||
const loginHelpLinks = wrapper.find('a');
|
||||
|
||||
expect(loginHelpLinks.at(0).prop('href')).toEqual('/register');
|
||||
expect(loginHelpLinks.at(1).prop('href')).toEqual(otherSignInIssues);
|
||||
});
|
||||
});
|
||||
@@ -12,8 +12,9 @@ import {
|
||||
injectIntl, intlShape, getCountryList, getLocale, FormattedMessage,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Form, Hyperlink, StatefulButton,
|
||||
Alert, Form, Hyperlink, StatefulButton,
|
||||
} from '@edx/paragon';
|
||||
import { Error } from '@edx/paragon/icons';
|
||||
import { closest } from 'fastest-levenshtein';
|
||||
|
||||
import {
|
||||
@@ -48,8 +49,8 @@ class RegistrationPage extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
sendPageEvent('login_and_registration', 'register');
|
||||
|
||||
const optionalFields = getConfig().REGISTRATION_OPTIONAL_FIELDS ? getConfig().REGISTRATION_OPTIONAL_FIELDS.split(',') : [];
|
||||
this.handleOnClose = this.handleOnClose.bind(this);
|
||||
|
||||
this.queryParams = getAllPossibleQueryParam();
|
||||
this.tpaHint = getTpaHint();
|
||||
@@ -73,6 +74,7 @@ class RegistrationPage extends React.Component {
|
||||
showOptionalField: false,
|
||||
startTime: Date.now(),
|
||||
optimizelyExperimentName: '',
|
||||
skipEmailValidation: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -220,7 +222,7 @@ class RegistrationPage extends React.Component {
|
||||
handleOnFocus = (e) => {
|
||||
const { errors } = this.state;
|
||||
errors[e.target.name] = '';
|
||||
this.setState({ errors });
|
||||
this.setState({ errors, skipEmailValidation: false });
|
||||
}
|
||||
|
||||
handleSuggestionClick = (suggestion) => {
|
||||
@@ -249,12 +251,12 @@ class RegistrationPage extends React.Component {
|
||||
errors.email = intl.formatMessage(messages['empty.email.field.error']);
|
||||
} else if (value.length <= 2 || !emailRegex.test(value)) {
|
||||
errors.email = intl.formatMessage(messages['email.invalid.format.error']);
|
||||
} else if (emailRegex.test(value)) {
|
||||
} else if (emailRegex.test(value) && !this.state.skipEmailValidation) {
|
||||
errors.email = '';
|
||||
let emailLexemes = value.split('@');
|
||||
let domainLexemes = emailLexemes[1].split('.');
|
||||
const serviceProvider = domainLexemes[0];
|
||||
const topLevelDomain = domainLexemes[1];
|
||||
const serviceProvider = domainLexemes.slice(-2)[0];
|
||||
const topLevelDomain = domainLexemes.slice(-1)[0];
|
||||
|
||||
if (DEFAULT_TOP_LEVEL_DOMAINS.indexOf(topLevelDomain) < 0) {
|
||||
let suggestedTld = closest(topLevelDomain, DEFAULT_TOP_LEVEL_DOMAINS);
|
||||
@@ -265,6 +267,7 @@ class RegistrationPage extends React.Component {
|
||||
suggestedTopLevelDomain: suggestedTld,
|
||||
suggestedServiceLevelDomain: '',
|
||||
borderClass: '',
|
||||
skipEmailValidation: false,
|
||||
});
|
||||
break;
|
||||
} else {
|
||||
@@ -336,6 +339,27 @@ class RegistrationPage extends React.Component {
|
||||
return errors;
|
||||
}
|
||||
|
||||
handleOnClose() {
|
||||
const { errors } = this.state;
|
||||
errors.email = '';
|
||||
this.setState({ errors, suggestedTopLevelDomain: '', skipEmailValidation: true });
|
||||
}
|
||||
|
||||
renderEmailFeedback() {
|
||||
if (this.state.suggestedTopLevelDomain) {
|
||||
return (
|
||||
<Alert variant="danger" onClose={this.handleOnClose} dismissible className="pb-1 pt-1" icon={Error}>
|
||||
{this.state.suggestedTopLevelDomain}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
if (this.state.suggestedServiceLevelDomain) {
|
||||
return <span className="one-rem-font">{this.state.suggestedServiceLevelDomain}</span>;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
renderThirdPartyAuth(providers, secondaryProviders, currentProvider, thirdPartyAuthApiStatus, intl) {
|
||||
const isInstitutionAuthActive = !!secondaryProviders.length && !currentProvider;
|
||||
const isSocialAuthActive = !!providers.length && !currentProvider;
|
||||
@@ -475,9 +499,9 @@ class RegistrationPage extends React.Component {
|
||||
helpText={[intl.formatMessage(messages['help.text.email'])]}
|
||||
floatingLabel={intl.formatMessage(messages['registration.email.label'])}
|
||||
borderClass={this.state.borderClass}
|
||||
suggestedTopLevelDomain={this.state.suggestedTopLevelDomain}
|
||||
suggestedServiceLevelDomain={this.state.suggestedServiceLevelDomain}
|
||||
/>
|
||||
>
|
||||
{this.renderEmailFeedback()}
|
||||
</FormGroup>
|
||||
|
||||
{!currentProvider && (
|
||||
<PasswordField
|
||||
|
||||
Reference in New Issue
Block a user