diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index f1cb2880bc..f5161f79b1 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -424,6 +424,7 @@ "somthing_went_wrong_compiling": "", "split_screen": "", "sso_link_error": "", + "start_by_adding_your_email": "", "start_free_trial": "", "stop_compile": "", "stop_on_validation_error": "", diff --git a/services/web/frontend/js/features/settings/components/emails/add-email.tsx b/services/web/frontend/js/features/settings/components/emails/add-email.tsx index db0914c0eb..94ceb5efea 100644 --- a/services/web/frontend/js/features/settings/components/emails/add-email.tsx +++ b/services/web/frontend/js/features/settings/components/emails/add-email.tsx @@ -14,6 +14,7 @@ import { ssoAvailableForDomain } from '../../utils/sso' import { postJSON } from '../../../../infrastructure/fetch-json' import { University } from '../../../../../../types/university' import { CountryCode } from '../../data/countries-list' +import { isValidEmail } from '../../../../shared/utils/email' function AddEmail() { const { t } = useTranslation() @@ -106,20 +107,44 @@ function AddEmail() { ) } + const InputCol = ( + + + + + + + ) + + if (!isValidEmail(newEmail)) { + return ( + +
+ {InputCol} + + +
{t('start_by_adding_your_email')}
+
+ + + + + + +
+
+ ) + } + return (
- - - - - - + {InputCol} {newEmailMatchedDomain && ssoAvailableForDomain(newEmailMatchedDomain) ? ( diff --git a/services/web/frontend/js/shared/utils/email.tsx b/services/web/frontend/js/shared/utils/email.tsx new file mode 100644 index 0000000000..a22375dfcd --- /dev/null +++ b/services/web/frontend/js/shared/utils/email.tsx @@ -0,0 +1,12 @@ +// Copied from backend code: https://github.com/overleaf/internal/blob/6af8ae850bd8075e6bf0ebcafd2731177cdf49ad/services/web/app/src/Features/Helpers/EmailHelper.js#L4 +const EMAIL_REGEXP = + // eslint-disable-next-line no-useless-escape + /^([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ + +export function isValidEmail(email: string | undefined | null) { + if (!email) { + return false + } else { + return EMAIL_REGEXP.test(email) + } +} diff --git a/services/web/test/frontend/features/settings/components/emails/emails-section-add-new-email.test.tsx b/services/web/test/frontend/features/settings/components/emails/emails-section-add-new-email.test.tsx index 8e49d3462f..a058ba2fc8 100644 --- a/services/web/test/frontend/features/settings/components/emails/emails-section-add-new-email.test.tsx +++ b/services/web/test/frontend/features/settings/components/emails/emails-section-add-new-email.test.tsx @@ -92,6 +92,54 @@ describe('', function () { screen.getByLabelText(/email/i) }) + it('renders "Start adding your address" until a valid email is typed', async function () { + fetchMock.get('/user/emails?ensureAffiliation=true', []) + render() + + const addAnotherEmailBtn = (await screen.findByRole('button', { + name: /add another email/i, + })) as HTMLButtonElement + fireEvent.click(addAnotherEmailBtn) + + const input = screen.getByLabelText(/email/i) + + // initially the text is displayed and the "add email" button disabled + screen.getByText('Start by adding your email address.') + expect( + ( + screen.getByRole('button', { + name: /add new email/i, + }) as HTMLButtonElement + ).disabled + ).to.be.true + + // no changes while writing the email address + fireEvent.change(input, { + target: { value: 'partial@email' }, + }) + screen.getByText('Start by adding your email address.') + expect( + ( + screen.getByRole('button', { + name: /add new email/i, + }) as HTMLButtonElement + ).disabled + ).to.be.true + + // the text is removed when the complete email address is typed, and the "add button" is reenabled + fireEvent.change(input, { + target: { value: 'valid@email.com' }, + }) + expect(screen.queryByText('Start by adding your email address.')).to.be.null + expect( + ( + screen.getByRole('button', { + name: /add new email/i, + }) as HTMLButtonElement + ).disabled + ).to.be.false + }) + it('renders "add new email" button', async function () { fetchMock.get('/user/emails?ensureAffiliation=true', []) render() diff --git a/services/web/test/frontend/shared/utils/email.test.tsx b/services/web/test/frontend/shared/utils/email.test.tsx new file mode 100644 index 0000000000..fd15f84b40 --- /dev/null +++ b/services/web/test/frontend/shared/utils/email.test.tsx @@ -0,0 +1,46 @@ +import { expect } from 'chai' +import { isValidEmail } from '../../../../frontend/js/shared/utils/email' + +const validEmailAddresses = [ + 'email@example.com', + 'firstname.lastname@example.com', + 'firstname-lastname@example.com', + 'email@subdomain.example.com', + 'firstname+lastname@example.com', + '1234567890@example.com', + 'email@example-one.com', + '_@example.com', + 'email@example.name', + 'email@example.co.jp', +] + +const invalidEmailAddresses = [ + 'plaintext', + '#@%^%#$@#$@#.com', + '@example.com', + 'email.example.com', + '.email@example.com', + 'email.@example.com', + 'email..email@example.com', + 'email@example.com (Joe Smith)', + 'email@example', + 'email@111.222.333.44444', + 'email@example..com', +] + +describe('isValidEmail', function () { + it('should return true for valid email addresses', function () { + validEmailAddresses.forEach(email => + expect(isValidEmail(email)).to.equal(true, email + ' should be valid ') + ) + }) + + it('should return false for invalid email addresses', function () { + invalidEmailAddresses.forEach(email => + expect(isValidEmail(email)).to.equal( + false, + email + ' should not be valid ' + ) + ) + }) +})