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

Tw email confirmation notification (#14088)

* Edited notification copy for user with affilation that ha commons account

* edited translation key to have overleaf professional appear in bold and switched to using trans component

* changed anchor tag to button and added additional check to ensure affiliation exists before accessing institution name

* add tests for commons user confirmation notification

* edit user email context test to facilitate test data changes

* edit license in fake test data

* edit test to check text of notification for non commons user

GitOrigin-RevId: 6700ceaa066f099ce593283887fa81d72ad624b6
Tyna William 3 лет назад
Родитель
Сommit
dd71c9924e

+ 3 - 0
services/web/frontend/extracted-translations.json

@@ -516,6 +516,7 @@
   "institution_account": "",
   "institution_acct_successfully_linked_2": "",
   "institution_and_role": "",
+  "institution_has_overleaf_subscription": "",
   "institution_templates": "",
   "institutional_leavers_survey_notification": "",
   "integrations": "",
@@ -704,6 +705,7 @@
   "ok": "",
   "on": "",
   "on_free_plan_upgrade_to_access_features": "",
+  "one_step_away_from_professional_features": "",
   "only_group_admin_or_managers_can_delete_your_account": "",
   "open_file": "",
   "open_link": "",
@@ -866,6 +868,7 @@
   "republish": "",
   "resend": "",
   "resend_confirmation_email": "",
+  "resend_email": "",
   "resending_confirmation_email": "",
   "resolve": "",
   "resolved_comments": "",

+ 0 - 1
services/web/frontend/js/features/project-list/components/notifications/groups/affiliation/reconfirmation-info.tsx

@@ -11,7 +11,6 @@ function ReconfirmationInfo() {
   ) as UserEmailData[]
   const userEmails = getMeta('ol-userEmails', []) as UserEmailData[]
   const reconfirmedViaSAML = getMeta('ol-reconfirmedViaSAML') as string
-
   return (
     <>
       {allInReconfirmNotificationPeriods.map(userEmail =>

+ 63 - 2
services/web/frontend/js/features/project-list/components/notifications/groups/confirm-email.tsx

@@ -1,4 +1,4 @@
-import { useTranslation } from 'react-i18next'
+import { Trans, useTranslation } from 'react-i18next'
 import { Button } from 'react-bootstrap'
 import Notification from '../notification'
 import Icon from '../../../../../shared/components/icon'
@@ -35,6 +35,28 @@ const ssoAvailable = ({ samlProviderId, affiliation }: UserEmailData) => {
   return false
 }
 
+function emailHasLicenceAfterConfirming(emailData: UserEmailData) {
+  if (emailData.confirmedAt) {
+    return false
+  }
+  if (!emailData.affiliation) {
+    return false
+  }
+  const affiliation = emailData.affiliation
+  const institution = affiliation.institution
+  if (!institution) {
+    return false
+  }
+  if (!institution.confirmed) {
+    return false
+  }
+  if (affiliation.pastReconfirmDate) {
+    return false
+  }
+
+  return affiliation.institution.commonsAccount
+}
+
 const showConfirmEmail = (userEmail: UserEmailData) => {
   const { emailConfirmationDisabled } = getMeta(
     'ol-ExposedSettings'
@@ -63,9 +85,48 @@ function ConfirmEmailNotification({ userEmail }: { userEmail: UserEmailData }) {
     return null
   }
 
+  if (emailHasLicenceAfterConfirming(userEmail)) {
+    return (
+      <Notification bsStyle="info">
+        <Notification.Body data-testid="notification-body">
+          {isLoading ? (
+            <>
+              <Icon type="spinner" spin /> {t('resending_confirmation_email')}
+              &hellip;
+            </>
+          ) : isError ? (
+            <div aria-live="polite">{getUserFacingMessage(error)}</div>
+          ) : (
+            <>
+              <Trans
+                i18nKey="one_step_away_from_professional_features"
+                components={[<strong />]} // eslint-disable-line react/jsx-key
+              />
+              <button
+                className="pull-right btn btn-info btn-sm"
+                onClick={() => handleResendConfirmationEmail(userEmail)}
+              >
+                {t('resend_email')}
+              </button>
+              <br />
+              <Trans
+                i18nKey="institution_has_overleaf_subscription"
+                values={{
+                  institutionName: userEmail.affiliation?.institution.name,
+                  emailAddress: userEmail.email,
+                }}
+                components={[<strong />]} // eslint-disable-line react/jsx-key
+              />
+            </>
+          )}
+        </Notification.Body>
+      </Notification>
+    )
+  }
+
   return (
     <Notification bsStyle="warning">
-      <Notification.Body>
+      <Notification.Body data-testid="pro-notification-body">
         {isLoading ? (
           <>
             <Icon type="spinner" spin /> {t('resending_confirmation_email')}

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

@@ -813,6 +813,7 @@
   "institution_acct_successfully_linked_2": "Your <0>__appName__</0> account was successfully linked to your <0>__institutionName__</0> institutional account.",
   "institution_and_role": "Institution and role",
   "institution_email_new_to_app": "Your <b>__institutionName__</b> email (<b>__email__</b>) is new to __appName__.",
+  "institution_has_overleaf_subscription": "<0>__institutionName__</0> has an Overleaf subscription. Click the confirmation link sent to __emailAddress__ to upgrade to <0>Overleaf Professional</0>.",
   "institution_templates": "Institution Templates",
   "institutional": "Institutional",
   "institutional_leavers_survey_notification": "Provide some quick feedback to receive a 25% discount on an annual subscription!",
@@ -1140,6 +1141,7 @@
   "one_collaborator": "Only one collaborator",
   "one_free_collab": "One free collaborator",
   "one_minute": "1 minute",
+  "one_step_away_from_professional_features": "You are one step away from accessing <0>Overleaf Professional features</0>!",
   "one_user": "1 user",
   "online_latex_editor": "Online LaTeX Editor",
   "only_group_admin_or_managers_can_delete_your_account": "Only your group admin or group managers will be able to delete your account.",
@@ -1402,6 +1404,7 @@
   "required": "Required",
   "resend": "Resend",
   "resend_confirmation_email": "Resend confirmation email",
+  "resend_email": "Resend email",
   "resending_confirmation_email": "Resending confirmation email",
   "reset_password": "Reset Password",
   "reset_your_password": "Reset your password",

+ 31 - 3
services/web/test/frontend/features/project-list/components/notifications.test.tsx

@@ -5,12 +5,14 @@ import {
   render,
   screen,
   waitForElementToBeRemoved,
+  within,
 } from '@testing-library/react'
 import fetchMock from 'fetch-mock'
 import { merge, cloneDeep } from 'lodash'
 import {
   professionalUserData,
   unconfirmedUserData,
+  unconfirmedCommonsUserData,
 } from '../../settings/fixtures/test-user-email-data'
 import {
   notification,
@@ -508,9 +510,6 @@ describe('<UserNotifications />', function () {
         emailConfirmationDisabled: false,
       })
       window.metaAttributesCache.set('ol-userEmails', [unconfirmedUserData])
-
-      renderWithinProjectListProvider(ConfirmEmail)
-      await fetchMock.flush(true)
     })
 
     afterEach(function () {
@@ -518,8 +517,16 @@ describe('<UserNotifications />', function () {
     })
 
     it('sends successfully', async function () {
+      renderWithinProjectListProvider(ConfirmEmail)
+      await fetchMock.flush(true)
       fetchMock.post('/user/emails/resend_confirmation', 200)
 
+      const email = unconfirmedUserData.email
+      const notificationBody = screen.getByTestId('pro-notification-body')
+      expect(notificationBody.textContent).to.contain(
+        `Please confirm your email ${email} by clicking on the link in the confirmation email`
+      )
+
       const resendButton = screen.getByRole('button', { name: /resend/i })
       fireEvent.click(resendButton)
 
@@ -534,6 +541,8 @@ describe('<UserNotifications />', function () {
     })
 
     it('fails to send', async function () {
+      renderWithinProjectListProvider(ConfirmEmail)
+      await fetchMock.flush(true)
       fetchMock.post('/user/emails/resend_confirmation', 500)
 
       const resendButton = screen.getByRole('button', { name: /resend/i })
@@ -546,6 +555,25 @@ describe('<UserNotifications />', function () {
       expect(fetchMock.called()).to.be.true
       screen.getByText(/something went wrong/i)
     })
+
+    it('shows notification for commons account', async function () {
+      window.metaAttributesCache.set('ol-userEmails', [
+        unconfirmedCommonsUserData,
+      ])
+
+      renderWithinProjectListProvider(ConfirmEmail)
+      await fetchMock.flush(true)
+
+      const alert = screen.getByRole('alert')
+      const email = unconfirmedCommonsUserData.email
+      const notificationBody = within(alert).getByTestId('notification-body')
+      expect(notificationBody.textContent).to.contain(
+        'You are one step away from accessing Overleaf Professional features'
+      )
+      expect(notificationBody.textContent).to.contain(
+        `Overleaf has an Overleaf subscription. Click the confirmation link sent to ${email} to upgrade to Overleaf Professional`
+      )
+    })
   })
 
   describe('<Affiliation/>', function () {

+ 2 - 0
services/web/test/frontend/features/settings/context/user-email-context.test.tsx

@@ -12,6 +12,7 @@ import {
   professionalUserData,
   unconfirmedUserData,
   fakeUsersData,
+  unconfirmedCommonsUserData,
 } from '../fixtures/test-user-email-data'
 import localStorage from '../../../../../frontend/js/infrastructure/local-storage'
 
@@ -53,6 +54,7 @@ describe('UserEmailContext', function () {
         'bar@overleaf.com': confirmedUserData,
         'baz@overleaf.com': unconfirmedUserData,
         'foo@overleaf.com': professionalUserData,
+        'qux@overleaf.com': unconfirmedCommonsUserData,
       })
       expect(result.current.state.data.linkedInstitutionIds).to.have.lengthOf(0)
 

+ 32 - 0
services/web/test/frontend/features/settings/fixtures/test-user-email-data.ts

@@ -44,6 +44,37 @@ export const professionalUserData: UserEmailData & {
   default: true,
 }
 
+export const unconfirmedCommonsUserData: UserEmailData & {
+  affiliation: Affiliation
+} = {
+  affiliation: {
+    cachedConfirmedAt: null,
+    cachedEntitlement: null,
+    cachedLastDayToReconfirm: null,
+    cachedPastReconfirmDate: false,
+    cachedReconfirmedAt: null,
+    department: 'Art History',
+    institution: {
+      commonsAccount: true,
+      confirmed: true,
+      id: 1,
+      isUniversity: false,
+      maxConfirmationMonths: null,
+      name: 'Overleaf',
+      ssoEnabled: false,
+      ssoBeta: false,
+    },
+    inReconfirmNotificationPeriod: false,
+    inferred: false,
+    licence: 'free',
+    pastReconfirmDate: false,
+    portal: { slug: '', templates_count: 1 },
+    role: 'Reader',
+  },
+  email: 'qux@overleaf.com',
+  default: true,
+}
+
 export const ssoUserData: UserEmailData = {
   affiliation: {
     cachedConfirmedAt: '2022-02-03T11:46:28.249Z',
@@ -79,4 +110,5 @@ export const fakeUsersData = [
   { ...confirmedUserData },
   { ...unconfirmedUserData },
   { ...professionalUserData },
+  { ...unconfirmedCommonsUserData },
 ]