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:
Waheed Ahmed
2021-06-11 18:05:40 +05:00
parent e80dbf8dc0
commit a9da4c6348
9 changed files with 59 additions and 211 deletions

View File

@@ -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=''

View File

@@ -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);

View File

@@ -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,

View File

@@ -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>

View File

@@ -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">

View File

@@ -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);

View File

@@ -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)}

View File

@@ -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);
});
});

View File

@@ -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