From bb59627db399dd62664adae3f75893ceb0a7f417 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Alby?= Date: Mon, 25 Apr 2022 13:04:55 +0200 Subject: [PATCH] Merge pull request #7697 from overleaf/msm-sso-flow-alerts [Settings] SSO Flow Alerts GitOrigin-RevId: fc89c86a6681b27e86bb6bf12f8bee51eb25aa8d --- .../web/frontend/extracted-translations.json | 4 + .../settings/components/emails/sso-alert.tsx | 101 ++++++++++++++++++ .../stories/settings/sso-alert.stories.tsx | 62 +++++++++++ services/web/locales/en.json | 3 + .../components/emails/sso-alert.test.tsx | 100 +++++++++++++++++ 5 files changed, 270 insertions(+) create mode 100644 services/web/frontend/js/features/settings/components/emails/sso-alert.tsx create mode 100644 services/web/frontend/stories/settings/sso-alert.stories.tsx create mode 100644 services/web/test/frontend/features/settings/components/emails/sso-alert.test.tsx diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 8a0f31fa98..4ccacf1639 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -194,6 +194,8 @@ "imported_from_the_output_of_another_project_at_date": "", "imported_from_zotero_at_date": "", "importing_and_merging_changes_in_github": "", + "in_order_to_match_institutional_metadata_2": "", + "institution_acct_successfully_linked_2": "", "institution_and_role": "", "integrations": "", "invalid_email": "", @@ -414,6 +416,7 @@ "template_approved_by_publisher": "", "terminated": "", "thanks_settings_updated": "", + "this_grants_access_to_features_2": "", "this_project_is_public": "", "this_project_is_public_read_only": "", "this_project_will_appear_in_your_dropbox_folder_at": "", @@ -426,6 +429,7 @@ "too_many_requests": "", "too_recently_compiled": "", "total_words": "", + "try_again": "", "try_it_for_free": "", "try_premium_for_free": "", "try_recompile_project": "", diff --git a/services/web/frontend/js/features/settings/components/emails/sso-alert.tsx b/services/web/frontend/js/features/settings/components/emails/sso-alert.tsx new file mode 100644 index 0000000000..664a09fd7a --- /dev/null +++ b/services/web/frontend/js/features/settings/components/emails/sso-alert.tsx @@ -0,0 +1,101 @@ +import { useState } from 'react' +import { useTranslation, Trans } from 'react-i18next' +import { Alert } from 'react-bootstrap' +import Icon from '../../../../shared/components/icon' +import getMeta from '../../../../utils/meta' + +type InstitutionLink = { + universityName: string + hasEntitlement?: boolean +} + +type SAMLError = { + translatedMessage?: string + message?: string + tryAgain?: boolean +} + +export function SSOAlert() { + const { t } = useTranslation() + + const institutionLinked: InstitutionLink | undefined = getMeta( + 'ol-institutionLinked' + ) + const institutionEmailNonCanonical: string | undefined = getMeta( + 'ol-institutionEmailNonCanonical' + ) + const samlError: SAMLError | undefined = getMeta('ol-samlError') + + const [infoClosed, setInfoClosed] = useState(false) + const [warningClosed, setWarningClosed] = useState(false) + const [errorClosed, setErrorClosed] = useState(false) + + const handleInfoClosed = () => setInfoClosed(true) + const handleWarningClosed = () => setWarningClosed(true) + const handleErrorClosed = () => setErrorClosed(true) + + if (samlError) { + return ( + !errorClosed && ( + +

+ {' '} + {samlError.translatedMessage + ? samlError.translatedMessage + : samlError.message} +

+ {samlError.tryAgain && ( +

{t('try_again')}

+ )} +
+ ) + ) + } + + if (!institutionLinked) { + return null + } + + return ( + <> + {!infoClosed && ( + +

+ ]} // eslint-disable-line react/jsx-key + values={{ institutionName: institutionLinked.universityName }} + /> +

+ {institutionLinked.hasEntitlement && ( +

+ ]} // eslint-disable-line react/jsx-key + values={{ featureType: t('professional') }} + /> +

+ )} +
+ )} + {!warningClosed && institutionEmailNonCanonical && ( + +

+ {' '} + ]} // eslint-disable-line react/jsx-key + values={{ email: institutionEmailNonCanonical }} + /> +

+
+ )} + + ) +} diff --git a/services/web/frontend/stories/settings/sso-alert.stories.tsx b/services/web/frontend/stories/settings/sso-alert.stories.tsx new file mode 100644 index 0000000000..529f61e8f1 --- /dev/null +++ b/services/web/frontend/stories/settings/sso-alert.stories.tsx @@ -0,0 +1,62 @@ +import EmailsSection from '../../js/features/settings/components/emails-section' +import { SSOAlert } from '../../js/features/settings/components/emails/sso-alert' + +export const Info = args => { + window.metaAttributesCache = new Map() + window.metaAttributesCache.set('ol-institutionLinked', { + universityName: 'Overleaf University', + }) + return +} + +export const InfoWithEntitlement = args => { + window.metaAttributesCache = new Map() + window.metaAttributesCache.set('ol-institutionLinked', { + universityName: 'Overleaf University', + hasEntitlement: true, + }) + return +} + +export const NonCanonicalEmail = args => { + window.metaAttributesCache = new Map() + window.metaAttributesCache.set('ol-institutionLinked', { + universityName: 'Overleaf University', + }) + window.metaAttributesCache.set( + 'ol-institutionEmailNonCanonical', + 'user@example.com' + ) + return +} + +export const Error = args => { + window.metaAttributesCache = new Map() + window.metaAttributesCache.set('ol-samlError', { + translatedMessage: 'There was an Error', + }) + return +} + +export const ErrorTranslated = args => { + window.metaAttributesCache = new Map() + window.metaAttributesCache.set('ol-samlError', { + translatedMessage: 'Translated Error Message', + message: 'There was an Error', + }) + return +} + +export const ErrorWithTryAgain = args => { + window.metaAttributesCache = new Map() + window.metaAttributesCache.set('ol-samlError', { + message: 'There was an Error', + tryAgain: true, + }) + return +} + +export default { + title: 'Account Settings / SSO Alerts', + component: EmailsSection, +} diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 55f0d91cbf..d9a71d44b9 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -167,6 +167,7 @@ "register_with_email_provided": "Register with __appName__ using the email and password you provided.", "security_reasons_linked_accts": "For security reasons, as your institutional email is already associated with the __email__ __appName__ account, we can only allow account linking with that specific account.", "this_grants_access_to_features": "This grants you access to __appName__ __featureType__ features.", + "this_grants_access_to_features_2": "This grants you access to <0>__appName__ <0>__featureType__ features.", "to_add_email_accounts_need_to_be_linked": "To add this email, your __appName__ and __institutionName__ accounts will need to be linked.", "tried_to_log_in_with_email": "You’ve tried to login with __email__.", "tried_to_register_with_email": "You’ve tried to register with __email__, which is already registered with __appName__ as an institutional account.", @@ -194,12 +195,14 @@ "if_owner_can_link": "If you own the __appName__ account with __email__, you will be allowed to link it to your __institutionName__ institutional account.", "ignore_and_continue_institution_linking": "You can also ignore this and continue to __appName__ with your __email__ account.", "in_order_to_match_institutional_metadata": "In order to match your institutional metadata, we’ve linked your account using __email__.", + "in_order_to_match_institutional_metadata_2": "In order to match your institutional metadata, we’ve linked your account using <0>__email__.", "in_order_to_match_institutional_metadata_associated": "In order to match your institutional metadata, your account is associated with the email __email__.", "institution_account_tried_to_add_already_registered": "The email/institution account you tried to add is already registered with __appName__.", "institution_account_tried_to_add_already_linked": "This institution is already linked with your account via another email address.", "institution_account_tried_to_add_not_affiliated": "This email is already associated with your account but not affiliated with this institution.", "institution_account_tried_to_add_affiliated_with_another_institution": "This email is already associated with your account but affiliated with another institution.", "institution_acct_successfully_linked": "Your __appName__ account was successfully linked to your __institutionName__ institutional account.", + "institution_acct_successfully_linked_2": "Your <0>__appName__ account was successfully linked to your <0>__institutionName__ institutional account.", "institution_account_tried_to_confirm_saml": "This email cannot be confirmed. Please remove the email from your account and try adding it again.", "institution_email_new_to_app": "Your __institutionName__ email (__email__) is new to __appName__.", "institutional": "Institutional", diff --git a/services/web/test/frontend/features/settings/components/emails/sso-alert.test.tsx b/services/web/test/frontend/features/settings/components/emails/sso-alert.test.tsx new file mode 100644 index 0000000000..7f408f27be --- /dev/null +++ b/services/web/test/frontend/features/settings/components/emails/sso-alert.test.tsx @@ -0,0 +1,100 @@ +import { render, screen, fireEvent } from '@testing-library/react' +import { expect } from 'chai' +import { SSOAlert } from '../../../../../../frontend/js/features/settings/components/emails/sso-alert' + +describe('', function () { + beforeEach(function () { + window.metaAttributesCache = new Map() + }) + + describe('when thereis no institutional linking information', function () { + it('should be empty', function () { + render() + expect(screen.queryByRole('alert')).to.be.null + }) + }) + + describe('when there is institutional linking information', function () { + beforeEach(function () { + window.metaAttributesCache.set('ol-institutionLinked', { + universityName: 'Overleaf University', + }) + }) + + it('should render an information alert with the university name', function () { + render() + screen.getByRole('alert') + screen.getByText('account was successfully linked', { exact: false }) + screen.getByText('Overleaf University', { exact: false }) + }) + + it('when entitled, it should render access granted to "professional" features', function () { + window.metaAttributesCache.get('ol-institutionLinked').hasEntitlement = + true + render() + screen.getByText('this grants you access', { exact: false }) + screen.getByText('Professional') + }) + + it('when the email is not canonical it should also render a warning alert', function () { + window.metaAttributesCache.set( + 'ol-institutionEmailNonCanonical', + 'user@example.com' + ) + render() + const alerts = screen.getAllByRole('alert') + expect(alerts.length).to.equal(2) + }) + + it('the alerts should be closeable', function () { + window.metaAttributesCache.set( + 'ol-institutionEmailNonCanonical', + 'user@example.com' + ) + render() + const closeButtons = screen.getAllByRole('button', { + name: 'Close alert', + }) + fireEvent.click(closeButtons[0]) + fireEvent.click(closeButtons[1]) + expect(screen.queryByRole('button', { name: 'Close alert' })).to.be.null + }) + }) + + describe('when there is a SAML Error', function () { + beforeEach(function () { + window.metaAttributesCache.set('ol-samlError', { + message: 'there was an error', + }) + }) + + it('should render an error alert', function () { + render() + screen.getByRole('alert') + screen.getByText('there was an error') + }) + + it('should render translated error if available', function () { + window.metaAttributesCache.get('ol-samlError').translatedMessage = + 'translated error' + render() + screen.getByText('translated error') + expect(screen.queryByText('there was an error')).to.be.null + }) + + it('should render a "try again" label when requested by the error payload', function () { + window.metaAttributesCache.get('ol-samlError').tryAgain = true + render() + screen.getByText('Please try again') + }) + + it('the alert should be closeable', function () { + render() + const closeButton = screen.getByRole('button', { + name: 'Close alert', + }) + fireEvent.click(closeButton) + expect(screen.queryByRole('button', { name: 'Close alert' })).to.be.null + }) + }) +})