Просмотр исходного кода

Merge pull request #7697 from overleaf/msm-sso-flow-alerts

[Settings] SSO Flow Alerts

GitOrigin-RevId: fc89c86a6681b27e86bb6bf12f8bee51eb25aa8d
Timothée Alby 4 лет назад
Родитель
Сommit
bb59627db3

+ 4 - 0
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": "",

+ 101 - 0
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 && (
+        <Alert bsStyle="danger" onDismiss={handleErrorClosed}>
+          <p className="text-center">
+            <Icon
+              type="exclamation-triangle"
+              accessibilityLabel={t('generic_something_went_wrong')}
+            />{' '}
+            {samlError.translatedMessage
+              ? samlError.translatedMessage
+              : samlError.message}
+          </p>
+          {samlError.tryAgain && (
+            <p className="text-center">{t('try_again')}</p>
+          )}
+        </Alert>
+      )
+    )
+  }
+
+  if (!institutionLinked) {
+    return null
+  }
+
+  return (
+    <>
+      {!infoClosed && (
+        <Alert bsStyle="info" onDismiss={handleInfoClosed}>
+          <p className="text-center">
+            <Trans
+              i18nKey="institution_acct_successfully_linked_2"
+              components={[<strong />]} // eslint-disable-line react/jsx-key
+              values={{ institutionName: institutionLinked.universityName }}
+            />
+          </p>
+          {institutionLinked.hasEntitlement && (
+            <p className="text-center">
+              <Trans
+                i18nKey="this_grants_access_to_features_2"
+                components={[<strong />]} // eslint-disable-line react/jsx-key
+                values={{ featureType: t('professional') }}
+              />
+            </p>
+          )}
+        </Alert>
+      )}
+      {!warningClosed && institutionEmailNonCanonical && (
+        <Alert bsStyle="warning" onDismiss={handleWarningClosed}>
+          <p className="text-center">
+            <Icon
+              type="exclamation-triangle"
+              accessibilityLabel={t('generic_something_went_wrong')}
+            />{' '}
+            <Trans
+              i18nKey="in_order_to_match_institutional_metadata_2"
+              components={[<strong />]} // eslint-disable-line react/jsx-key
+              values={{ email: institutionEmailNonCanonical }}
+            />
+          </p>
+        </Alert>
+      )}
+    </>
+  )
+}

+ 62 - 0
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 <SSOAlert />
+}
+
+export const InfoWithEntitlement = args => {
+  window.metaAttributesCache = new Map()
+  window.metaAttributesCache.set('ol-institutionLinked', {
+    universityName: 'Overleaf University',
+    hasEntitlement: true,
+  })
+  return <SSOAlert />
+}
+
+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 <SSOAlert />
+}
+
+export const Error = args => {
+  window.metaAttributesCache = new Map()
+  window.metaAttributesCache.set('ol-samlError', {
+    translatedMessage: 'There was an Error',
+  })
+  return <SSOAlert />
+}
+
+export const ErrorTranslated = args => {
+  window.metaAttributesCache = new Map()
+  window.metaAttributesCache.set('ol-samlError', {
+    translatedMessage: 'Translated Error Message',
+    message: 'There was an Error',
+  })
+  return <SSOAlert />
+}
+
+export const ErrorWithTryAgain = args => {
+  window.metaAttributesCache = new Map()
+  window.metaAttributesCache.set('ol-samlError', {
+    message: 'There was an Error',
+    tryAgain: true,
+  })
+  return <SSOAlert />
+}
+
+export default {
+  title: 'Account Settings / SSO Alerts',
+  component: EmailsSection,
+}

+ 3 - 0
services/web/locales/en.json

@@ -167,6 +167,7 @@
   "register_with_email_provided": "<a href=\"__link__\">Register with __appName__</a> using the email and password you provided.",
   "security_reasons_linked_accts": "For security reasons, as your institutional email is already associated with the <b>__email__</b> <b>__appName__</b> account, we can only allow account linking with that specific account.",
   "this_grants_access_to_features": "This grants you access to <b>__appName__</b> <b>__featureType__</b> features.",
+  "this_grants_access_to_features_2": "This grants you access to <0>__appName__</0> <0>__featureType__</0> features.",
   "to_add_email_accounts_need_to_be_linked": "To add this email, your <b>__appName__</b> and <b>__institutionName__</b> accounts will need to be linked.",
   "tried_to_log_in_with_email": "You’ve tried to login with <b>__email__</b>.",
   "tried_to_register_with_email": "You’ve tried to register with <b>__email__</b>, which is already registered with <b>__appName__</b> as an institutional account.",
@@ -194,12 +195,14 @@
   "if_owner_can_link": "If you own the <b>__appName__</b> account with <b>__email__</b>, you will be allowed to link it to your <b>__institutionName__</b> institutional account.",
   "ignore_and_continue_institution_linking": "You can also ignore this and <a href=\"__link__\">continue to __appName__ with your <b>__email__</b> account</a>.",
   "in_order_to_match_institutional_metadata": "In order to match your institutional metadata, we’ve linked your account using <b>__email__</b>.",
+  "in_order_to_match_institutional_metadata_2": "In order to match your institutional metadata, we’ve linked your account using <0>__email__</0>.",
   "in_order_to_match_institutional_metadata_associated": "In order to match your institutional metadata, your account is associated with the email <b>__email__</b>.",
   "institution_account_tried_to_add_already_registered": "The email/institution account you tried to add is <b>already registered</b> with  __appName__.",
   "institution_account_tried_to_add_already_linked": "This institution is <b>already linked</b> with your account via another email address.",
   "institution_account_tried_to_add_not_affiliated": "This email is <b>already associated</b> with your account but not affiliated with this institution.",
   "institution_account_tried_to_add_affiliated_with_another_institution": "This email is <b>already associated</b> with your account but affiliated with another institution.",
   "institution_acct_successfully_linked": "Your <b>__appName__</b> account was successfully linked to your <b>__institutionName__</b> institutional account.",
+  "institution_acct_successfully_linked_2": "Your <0>__appName__</0> account was successfully linked to your <0>__institutionName__</0> 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 <b>__institutionName__</b> email (<b>__email__</b>) is new to __appName__.",
   "institutional": "Institutional",

+ 100 - 0
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('<SSOAlert/>', function () {
+  beforeEach(function () {
+    window.metaAttributesCache = new Map()
+  })
+
+  describe('when thereis no institutional linking information', function () {
+    it('should be empty', function () {
+      render(<SSOAlert />)
+      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(<SSOAlert />)
+      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(<SSOAlert />)
+      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(<SSOAlert />)
+      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(<SSOAlert />)
+      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(<SSOAlert />)
+      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(<SSOAlert />)
+      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(<SSOAlert />)
+      screen.getByText('Please try again')
+    })
+
+    it('the alert should be closeable', function () {
+      render(<SSOAlert />)
+      const closeButton = screen.getByRole('button', {
+        name: 'Close alert',
+      })
+      fireEvent.click(closeButton)
+      expect(screen.queryByRole('button', { name: 'Close alert' })).to.be.null
+    })
+  })
+})