Explorar o código

Merge pull request #15946 from overleaf/msm-new-notification-others

[web] new notification style for dashboard alerts

GitOrigin-RevId: cf5bea32a89f35d63ca4bdc2df2619352d421ff3
Miguel Serrano %!s(int64=2) %!d(string=hai) anos
pai
achega
a04b5c0418
Modificáronse 16 ficheiros con 645 adicións e 203 borrados
  1. 9 4
      services/web/frontend/js/features/project-list/components/notifications/ads/inr-banner.tsx
  2. 6 2
      services/web/frontend/js/features/project-list/components/notifications/ads/latam-banner.tsx
  3. 9 1
      services/web/frontend/js/features/project-list/components/notifications/groups-and-enterprise-banner.tsx
  4. 5 1
      services/web/frontend/js/features/project-list/components/notifications/groups/affiliation/reconfirm-affiliation.tsx
  5. 6 8
      services/web/frontend/js/features/project-list/components/notifications/groups/affiliation/reconfirmation-info.tsx
  6. 18 9
      services/web/frontend/js/features/project-list/components/notifications/groups/common.tsx
  7. 58 52
      services/web/frontend/js/features/project-list/components/notifications/groups/confirm-email.tsx
  8. 32 20
      services/web/frontend/js/features/project-list/components/notifications/groups/group-invitation/group-invitation-cancel-subscription.tsx
  9. 14 9
      services/web/frontend/js/features/project-list/components/notifications/groups/group-invitation/group-invitation-join.tsx
  10. 17 6
      services/web/frontend/js/features/project-list/components/notifications/groups/group-invitation/group-invitation-successful.tsx
  11. 98 78
      services/web/frontend/js/features/project-list/components/notifications/groups/institution.tsx
  12. 9 7
      services/web/frontend/js/features/project-list/components/notifications/notification.tsx
  13. 13 4
      services/web/frontend/js/features/project-list/components/notifications/user-notifications.tsx
  14. 1 1
      services/web/frontend/js/features/project-list/components/notifications/writefull-premium-promo-banner.tsx
  15. 10 1
      services/web/frontend/js/features/project-list/components/notifications/writefull-promo-banner.tsx
  16. 340 0
      services/web/frontend/stories/project-list/notifications-new.stories.tsx

+ 9 - 4
services/web/frontend/js/features/project-list/components/notifications/ads/inr-banner.tsx

@@ -5,6 +5,7 @@ import Notification from '../notification'
 import * as eventTracking from '../../../../../infrastructure/event-tracking'
 import { Modal, Button } from 'react-bootstrap'
 import AccessibleModal from '../../../../../shared/components/accessible-modal'
+import getMeta from '@/utils/meta'
 
 interface VariantContents {
   default: string
@@ -25,6 +26,10 @@ type INRBannerProps = {
 
 export default function INRBanner({ variant, splitTestName }: INRBannerProps) {
   const { t } = useTranslation()
+  const newNotificationStyle = getMeta(
+    'ol-newNotificationStyle',
+    false
+  ) as boolean
   const [dismissedUntil, setDismissedUntil] = usePersistedState<
     Date | undefined
   >(`has_dismissed_inr_banner_until`)
@@ -107,9 +112,9 @@ export default function INRBanner({ variant, splitTestName }: INRBannerProps) {
         }
         action={
           <Button
-            bsStyle="info"
+            bsStyle={newNotificationStyle ? null : 'info'}
             bsSize="sm"
-            className="pull-right"
+            className={newNotificationStyle ? 'btn-secondary' : 'pull-right'}
             onClick={handleClick}
           >
             {t('get_discounted_plan')}
@@ -133,9 +138,9 @@ export default function INRBanner({ variant, splitTestName }: INRBannerProps) {
         }
         action={
           <Button
-            bsStyle="success"
+            bsStyle={newNotificationStyle ? null : 'success'}
             bsSize="sm"
-            className="pull-right"
+            className={newNotificationStyle ? 'btn-secondary' : 'pull-right'}
             onClick={handleClick}
           >
             {t('get_discounted_plan')} ₹

+ 6 - 2
services/web/frontend/js/features/project-list/components/notifications/ads/latam-banner.tsx

@@ -16,6 +16,10 @@ const LATAM_CURRENCIES = {
 
 export default function LATAMBanner() {
   const { t } = useTranslation()
+  const newNotificationStyle = getMeta(
+    'ol-newNotificationStyle',
+    false
+  ) as boolean
   const [dismissedAt, setDismissedAt] = usePersistedState<Date | undefined>(
     `has_dismissed_latam_banner`
   )
@@ -97,9 +101,9 @@ export default function LATAMBanner() {
       }
       action={
         <Button
-          bsStyle="info"
+          bsStyle={newNotificationStyle ? null : 'info'}
           bsSize="sm"
-          className="pull-right"
+          className={newNotificationStyle ? 'btn-secondary' : 'pull-right'}
           onClick={handleClick}
         >
           {t('get_discounted_plan')}

+ 9 - 1
services/web/frontend/js/features/project-list/components/notifications/groups-and-enterprise-banner.tsx

@@ -20,6 +20,10 @@ export default function GroupsAndEnterpriseBanner() {
   )
   const groupsAndEnterpriseBannerVariant: GroupsAndEnterpriseBannerVariant =
     getMeta('ol-groupsAndEnterpriseBannerVariant')
+  const newNotificationStyle = getMeta(
+    'ol-newNotificationStyle',
+    false
+  ) as boolean
 
   const hasDismissedGroupsAndEnterpriseBanner = hasRecentlyDismissedBanner()
 
@@ -68,7 +72,11 @@ export default function GroupsAndEnterpriseBanner() {
       body={<BannerContent variant={groupsAndEnterpriseBannerVariant} />}
       action={
         <a
-          className="pull-right btn btn-info btn-sm"
+          className={
+            newNotificationStyle
+              ? 'btn btn-secondary btn-sm'
+              : 'pull-right btn btn-info btn-sm'
+          }
           href={contactSalesUrl}
           target="_blank"
           rel="noreferrer"

+ 5 - 1
services/web/frontend/js/features/project-list/components/notifications/groups/affiliation/reconfirm-affiliation.tsx

@@ -25,6 +25,10 @@ function ReconfirmAffiliation({
 }: ReconfirmAffiliationProps) {
   const { t } = useTranslation()
   const { samlInitPath } = getMeta('ol-ExposedSettings') as ExposedSettings
+  const newNotificationStyle = getMeta(
+    'ol-newNotificationStyle',
+    false
+  ) as boolean
   const { error, isLoading, isError, isSuccess, runAsync } = useAsync()
   const [hasSent, setHasSent] = useState(false)
   const [isPending, setIsPending] = useState(false)
@@ -95,7 +99,7 @@ function ReconfirmAffiliation({
 
   return (
     <div className="w-100">
-      <Icon type="warning" />
+      {!newNotificationStyle && <Icon type="warning" />}
       <Button
         bsStyle="info"
         bsSize="sm"

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

@@ -18,16 +18,15 @@ function ReconfirmationInfo() {
           <Notification
             key={`reconfirmation-period-email-${userEmail.email}`}
             bsStyle="info"
-          >
-            <Notification.Body>
+            body={
               <div className="reconfirm-notification">
                 <ReconfirmAffiliation
                   email={userEmail.email}
                   institution={userEmail.affiliation.institution}
                 />
               </div>
-            </Notification.Body>
-          </Notification>
+            }
+          />
         ) : null
       )}
       {userEmails.map(userEmail =>
@@ -37,13 +36,12 @@ function ReconfirmationInfo() {
             key={`samlIdentifier-email-${userEmail.email}`}
             bsStyle="info"
             onDismiss={() => {}}
-          >
-            <Notification.Body>
+            body={
               <ReconfirmationInfoSuccess
                 institution={userEmail.affiliation?.institution}
               />
-            </Notification.Body>
-          </Notification>
+            }
+          />
         ) : null
       )}
     </>

+ 18 - 9
services/web/frontend/js/features/project-list/components/notifications/groups/common.tsx

@@ -38,6 +38,10 @@ function CommonNotification({ notification }: CommonNotificationProps) {
   const { t } = useTranslation()
   const { samlInitPath } = getMeta('ol-ExposedSettings') as ExposedSettings
   const user = getMeta('ol-user', []) as Pick<User, 'features'>
+  const newNotificationStyle = getMeta(
+    'ol-newNotificationStyle',
+    false
+  ) as boolean
   const { isLoading, isSuccess, error, runAsync } = useAsync<
     never,
     FetchError
@@ -90,16 +94,21 @@ function CommonNotification({ notification }: CommonNotificationProps) {
           action={
             accepted ? (
               <Button
-                bsStyle="info"
+                bsStyle={newNotificationStyle ? null : 'info'}
                 bsSize="sm"
-                className="pull-right"
+                className={
+                  newNotificationStyle ? 'btn-secondary' : 'pull-right'
+                }
                 href={`/project/${notification.messageOpts.projectId}`}
               >
                 {t('open_project')}
               </Button>
             ) : (
               <Button
-                bsStyle="info"
+                bsStyle={newNotificationStyle ? null : 'info'}
+                className={
+                  newNotificationStyle ? 'btn-secondary' : 'pull-right'
+                }
                 bsSize="sm"
                 disabled={isLoading}
                 onClick={() => handleAcceptInvite(notification)}
@@ -128,9 +137,9 @@ function CommonNotification({ notification }: CommonNotificationProps) {
           }
           action={
             <Button
-              bsStyle="info"
+              bsStyle={newNotificationStyle ? null : 'info'}
               bsSize="sm"
-              className="pull-right"
+              className={newNotificationStyle ? 'btn-secondary' : 'pull-right'}
               href="https://www.overleaf.com/events/wfh2020"
             >
               View
@@ -189,9 +198,9 @@ function CommonNotification({ notification }: CommonNotificationProps) {
           }
           action={
             <Button
-              bsStyle="info"
+              bsStyle={newNotificationStyle ? null : 'info'}
               bsSize="sm"
-              className="pull-right"
+              className={newNotificationStyle ? 'btn-secondary' : 'pull-right'}
               href={
                 notification.messageOpts.ssoEnabled
                   ? `${samlInitPath}?university_id=${notification.messageOpts.institutionId}&auto=/project`
@@ -219,9 +228,9 @@ function CommonNotification({ notification }: CommonNotificationProps) {
           }
           action={
             <Button
-              bsStyle="danger"
+              bsStyle={newNotificationStyle ? null : 'danger'}
               bsSize="sm"
-              className="pull-right"
+              className={newNotificationStyle ? 'btn-secondary' : 'pull-right'}
               href="/user/settings"
             >
               Account Settings

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

@@ -103,8 +103,53 @@ function ConfirmEmailNotification({ userEmail }: { userEmail: UserEmailData }) {
   // already have premium features.
   if (emailHasLicenceAfterConfirming(userEmail) && isOnFreeOrIndividualPlan()) {
     return (
-      <Notification bsStyle="info">
-        <Notification.Body data-testid="notification-body">
+      <Notification
+        bsStyle="info"
+        body={
+          <div 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,
+                  }}
+                  shouldUnescape
+                  tOptions={{ interpolation: { escapeValue: true } }}
+                  components={[<strong />]} // eslint-disable-line react/jsx-key
+                />
+              </>
+            )}
+          </div>
+        }
+      />
+    )
+  }
+
+  return (
+    <Notification
+      bsStyle="warning"
+      body={
+        <div data-testid="pro-notification-body">
           {isLoading ? (
             <>
               <Icon type="spinner" spin /> {t('resending_confirmation_email')}
@@ -114,60 +159,21 @@ function ConfirmEmailNotification({ userEmail }: { userEmail: UserEmailData }) {
             <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"
+              {t('please_confirm_email', {
+                emailAddress: userEmail.email,
+              })}{' '}
+              <Button
+                bsStyle="link"
+                className="btn-inline-link"
                 onClick={() => handleResendConfirmationEmail(userEmail)}
               >
-                {t('resend_email')}
-              </button>
-              <br />
-              <Trans
-                i18nKey="institution_has_overleaf_subscription"
-                values={{
-                  institutionName: userEmail.affiliation?.institution.name,
-                  emailAddress: userEmail.email,
-                }}
-                shouldUnescape
-                tOptions={{ interpolation: { escapeValue: true } }}
-                components={[<strong />]} // eslint-disable-line react/jsx-key
-              />
+                ({t('resend_confirmation_email')})
+              </Button>
             </>
           )}
-        </Notification.Body>
-      </Notification>
-    )
-  }
-
-  return (
-    <Notification bsStyle="warning">
-      <Notification.Body data-testid="pro-notification-body">
-        {isLoading ? (
-          <>
-            <Icon type="spinner" spin /> {t('resending_confirmation_email')}
-            &hellip;
-          </>
-        ) : isError ? (
-          <div aria-live="polite">{getUserFacingMessage(error)}</div>
-        ) : (
-          <>
-            {t('please_confirm_email', {
-              emailAddress: userEmail.email,
-            })}{' '}
-            <Button
-              bsStyle="link"
-              className="btn-inline-link"
-              onClick={() => handleResendConfirmationEmail(userEmail)}
-            >
-              ({t('resend_confirmation_email')})
-            </Button>
-          </>
-        )}
-      </Notification.Body>
-    </Notification>
+        </div>
+      }
+    />
   )
 }
 

+ 32 - 20
services/web/frontend/js/features/project-list/components/notifications/groups/group-invitation/group-invitation-cancel-subscription.tsx

@@ -4,6 +4,7 @@ import type { Dispatch, SetStateAction } from 'react'
 import Notification from '../../notification'
 import { GroupInvitationStatus } from './hooks/use-group-invitation-notification'
 import type { NotificationGroupInvitation } from '../../../../../../../../types/project/dashboard/notification'
+import getMeta from '@/utils/meta'
 
 type GroupInvitationCancelIndividualSubscriptionNotificationProps = {
   setGroupInvitationStatus: Dispatch<SetStateAction<GroupInvitationStatus>>
@@ -19,30 +20,41 @@ export default function GroupInvitationCancelIndividualSubscriptionNotification(
   notification,
 }: GroupInvitationCancelIndividualSubscriptionNotificationProps) {
   const { t } = useTranslation()
+  const newNotificationStyle = getMeta(
+    'ol-newNotificationStyle',
+    false
+  ) as boolean
   const {
     messageOpts: { inviterName },
   } = notification
 
   return (
-    <Notification bsStyle="info" onDismiss={dismissGroupInviteNotification}>
-      <Notification.Body>
-        {t('invited_to_group_have_individual_subcription', { inviterName })}
-      </Notification.Body>
-      <Notification.Action className="group-invitation-cancel-subscription-notification-buttons">
-        <Button
-          bsStyle="info"
-          bsSize="sm"
-          className="me-1"
-          onClick={() =>
-            setGroupInvitationStatus(GroupInvitationStatus.AskToJoin)
-          }
-        >
-          {t('not_now')}
-        </Button>
-        <Button bsStyle="info" bsSize="sm" onClick={cancelPersonalSubscription}>
-          {t('cancel_my_subscription')}
-        </Button>
-      </Notification.Action>
-    </Notification>
+    <Notification
+      bsStyle="info"
+      onDismiss={dismissGroupInviteNotification}
+      body={t('invited_to_group_have_individual_subcription', { inviterName })}
+      action={
+        <div className="group-invitation-cancel-subscription-notification-buttons">
+          <Button
+            bsStyle={newNotificationStyle ? null : 'info'}
+            bsSize="sm"
+            className={newNotificationStyle ? 'me-1 btn-secondary' : 'me-1'}
+            onClick={() =>
+              setGroupInvitationStatus(GroupInvitationStatus.AskToJoin)
+            }
+          >
+            {t('not_now')}
+          </Button>
+          <Button
+            bsStyle={newNotificationStyle ? null : 'info'}
+            className={newNotificationStyle ? 'btn-secondary' : ''}
+            bsSize="sm"
+            onClick={cancelPersonalSubscription}
+          >
+            {t('cancel_my_subscription')}
+          </Button>
+        </div>
+      }
+    />
   )
 }

+ 14 - 9
services/web/frontend/js/features/project-list/components/notifications/groups/group-invitation/group-invitation-join.tsx

@@ -2,6 +2,7 @@ import { Button } from 'react-bootstrap'
 import { useTranslation } from 'react-i18next'
 import Notification from '../../notification'
 import type { NotificationGroupInvitation } from '../../../../../../../../types/project/dashboard/notification'
+import getMeta from '@/utils/meta'
 
 type GroupInvitationNotificationProps = {
   acceptGroupInvite: () => void
@@ -17,26 +18,30 @@ export default function GroupInvitationNotificationJoin({
   dismissGroupInviteNotification,
 }: GroupInvitationNotificationProps) {
   const { t } = useTranslation()
+  const newNotificationStyle = getMeta(
+    'ol-newNotificationStyle',
+    false
+  ) as boolean
   const {
     messageOpts: { inviterName },
   } = notification
 
   return (
-    <Notification bsStyle="info" onDismiss={dismissGroupInviteNotification}>
-      <Notification.Body>
-        {t('invited_to_group', { inviterName })}
-      </Notification.Body>
-      <Notification.Action>
+    <Notification
+      bsStyle="info"
+      onDismiss={dismissGroupInviteNotification}
+      body={t('invited_to_group', { inviterName })}
+      action={
         <Button
-          bsStyle="info"
+          bsStyle={newNotificationStyle ? null : 'info'}
           bsSize="sm"
-          className="pull-right"
+          className={newNotificationStyle ? 'btn-secondary' : 'pull-right'}
           onClick={acceptGroupInvite}
           disabled={isAcceptingInvitation}
         >
           {t('join_now')}
         </Button>
-      </Notification.Action>
-    </Notification>
+      }
+    />
   )
 }

+ 17 - 6
services/web/frontend/js/features/project-list/components/notifications/groups/group-invitation/group-invitation-successful.tsx

@@ -1,6 +1,7 @@
 import Notification from '../../notification'
 import { useTranslation } from 'react-i18next'
 import Icon from '../../../../../../shared/components/icon'
+import getMeta from '@/utils/meta'
 
 type GroupInvitationSuccessfulNotificationProps = {
   hideNotification: () => void
@@ -10,13 +11,23 @@ export default function GroupInvitationSuccessfulNotification({
   hideNotification,
 }: GroupInvitationSuccessfulNotificationProps) {
   const { t } = useTranslation()
+  const newNotificationStyle = getMeta(
+    'ol-newNotificationStyle',
+    false
+  ) as boolean
 
   return (
-    <Notification bsStyle="success" onDismiss={hideNotification}>
-      <Notification.Body>
-        <Icon type="check-circle" fw aria-hidden="true" className="me-1" />
-        {t('congratulations_youve_successfully_join_group')}
-      </Notification.Body>
-    </Notification>
+    <Notification
+      bsStyle="success"
+      onDismiss={hideNotification}
+      body={
+        <>
+          {!newNotificationStyle && (
+            <Icon type="check-circle" fw aria-hidden="true" className="me-1" />
+          )}
+          {t('congratulations_youve_successfully_join_group')}
+        </>
+      }
+    />
   )
 }

+ 98 - 78
services/web/frontend/js/features/project-list/components/notifications/groups/institution.tsx

@@ -17,6 +17,10 @@ function Institution() {
     'ol-notificationsInstitution',
     []
   ) as InstitutionType[]
+  const newNotificationStyle = getMeta(
+    'ol-newNotificationStyle',
+    false
+  ) as boolean
   const { handleDismiss } = useAsyncDismiss()
 
   if (!notificationsInstitution.length) {
@@ -41,47 +45,51 @@ function Institution() {
         ) => (
           <Fragment key={index}>
             {templateKey === 'notification_institution_sso_available' && (
-              <Notification bsStyle="info">
-                <Notification.Body>
-                  <p>
-                    <Trans
-                      i18nKey="can_link_institution_email_acct_to_institution_acct"
-                      components={{ b: <b /> }}
-                      values={{ appName, email, institutionName }}
-                      shouldUnescape
-                      tOptions={{ interpolation: { escapeValue: true } }}
-                    />
-                  </p>
-                  <div>
-                    <Trans
-                      i18nKey="doing_this_allow_log_in_through_institution"
-                      components={{ b: <b /> }}
-                      values={{ appName }}
-                      shouldUnescape
-                      tOptions={{ interpolation: { escapeValue: true } }}
-                    />{' '}
-                    <a href="/learn/how-to/Institutional_Login">
-                      {t('learn_more')}
-                    </a>
-                  </div>
-                </Notification.Body>
-                <Notification.Action>
+              <Notification
+                bsStyle="info"
+                body={
+                  <>
+                    {' '}
+                    <p>
+                      <Trans
+                        i18nKey="can_link_institution_email_acct_to_institution_acct"
+                        components={{ b: <b /> }}
+                        values={{ appName, email, institutionName }}
+                        shouldUnescape
+                        tOptions={{ interpolation: { escapeValue: true } }}
+                      />
+                    </p>
+                    <div>
+                      <Trans
+                        i18nKey="doing_this_allow_log_in_through_institution"
+                        components={{ b: <b /> }}
+                        values={{ appName }}
+                        shouldUnescape
+                        tOptions={{ interpolation: { escapeValue: true } }}
+                      />{' '}
+                      <a href="/learn/how-to/Institutional_Login">
+                        {t('learn_more')}
+                      </a>
+                    </div>
+                  </>
+                }
+                action={
                   <Button
-                    bsStyle="info"
+                    bsStyle={newNotificationStyle ? null : 'info'}
+                    className={newNotificationStyle ? 'btn-secondary' : ''}
                     bsSize="sm"
                     href={`${samlInitPath}?university_id=${institutionId}&auto=/project&email=${email}`}
                   >
                     {t('link_account')}
                   </Button>
-                </Notification.Action>
-              </Notification>
+                }
+              />
             )}
             {templateKey === 'notification_institution_sso_linked' && (
               <Notification
                 bsStyle="info"
                 onDismiss={() => id && handleDismiss(id)}
-              >
-                <Notification.Body>
+                body={
                   <Trans
                     i18nKey="account_has_been_link_to_institution_account"
                     components={{ b: <b /> }}
@@ -89,76 +97,88 @@ function Institution() {
                     shouldUnescape
                     tOptions={{ interpolation: { escapeValue: true } }}
                   />
-                </Notification.Body>
-              </Notification>
+                }
+              />
             )}
             {templateKey === 'notification_institution_sso_non_canonical' && (
               <Notification
                 bsStyle="warning"
                 onDismiss={() => id && handleDismiss(id)}
-              >
-                <Notification.Body>
-                  <Icon type="exclamation-triangle" fw />
-                  <Trans
-                    i18nKey="tried_to_log_in_with_email"
-                    components={{ b: <b /> }}
-                    values={{ appName, email: requestedEmail }}
-                    shouldUnescape
-                    tOptions={{ interpolation: { escapeValue: true } }}
-                  />{' '}
-                  <Trans
-                    i18nKey="in_order_to_match_institutional_metadata_associated"
-                    components={{ b: <b /> }}
-                    values={{ email: institutionEmail }}
-                    shouldUnescape
-                    tOptions={{ interpolation: { escapeValue: true } }}
-                  />
-                </Notification.Body>
-              </Notification>
+                body={
+                  <>
+                    {!newNotificationStyle && (
+                      <>
+                        <Icon type="exclamation-triangle" fw />{' '}
+                      </>
+                    )}
+                    <Trans
+                      i18nKey="tried_to_log_in_with_email"
+                      components={{ b: <b /> }}
+                      values={{ appName, email: requestedEmail }}
+                      shouldUnescape
+                      tOptions={{ interpolation: { escapeValue: true } }}
+                    />{' '}
+                    <Trans
+                      i18nKey="in_order_to_match_institutional_metadata_associated"
+                      components={{ b: <b /> }}
+                      values={{ email: institutionEmail }}
+                      shouldUnescape
+                      tOptions={{ interpolation: { escapeValue: true } }}
+                    />
+                  </>
+                }
+              />
             )}
             {templateKey ===
               'notification_institution_sso_already_registered' && (
               <Notification
                 bsStyle="info"
                 onDismiss={() => id && handleDismiss(id)}
-              >
-                <Notification.Body>
-                  <Trans
-                    i18nKey="tried_to_register_with_email"
-                    components={{ b: <b /> }}
-                    values={{ appName, email }}
-                    shouldUnescape
-                    tOptions={{ interpolation: { escapeValue: true } }}
-                  />{' '}
-                  {t('we_logged_you_in')}
-                </Notification.Body>
-                <Notification.Action>
+                body={
+                  <>
+                    <Trans
+                      i18nKey="tried_to_register_with_email"
+                      components={{ b: <b /> }}
+                      values={{ appName, email }}
+                      shouldUnescape
+                      tOptions={{ interpolation: { escapeValue: true } }}
+                    />{' '}
+                    {t('we_logged_you_in')}
+                  </>
+                }
+                action={
                   <Button
-                    bsStyle="info"
+                    bsStyle={newNotificationStyle ? null : 'info'}
+                    className={newNotificationStyle ? 'btn-secondary' : ''}
                     bsSize="sm"
                     href="/learn/how-to/Institutional_Login"
                   >
                     {t('find_out_more')}
                   </Button>
-                </Notification.Action>
-              </Notification>
+                }
+              />
             )}
             {templateKey === 'notification_institution_sso_error' && (
               <Notification
                 bsStyle="danger"
                 onDismiss={() => id && handleDismiss(id)}
-              >
-                <Notification.Body>
-                  <Icon type="exclamation-triangle" fw />{' '}
-                  {t('generic_something_went_wrong')}.
-                  <div>
-                    {error?.translatedMessage
-                      ? error?.translatedMessage
-                      : error?.message}
-                  </div>
-                  {error?.tryAgain ? `${t('try_again')}.` : null}
-                </Notification.Body>
-              </Notification>
+                body={
+                  <>
+                    {!newNotificationStyle && (
+                      <>
+                        <Icon type="exclamation-triangle" fw />{' '}
+                      </>
+                    )}
+                    {t('generic_something_went_wrong')}.
+                    <div>
+                      {error?.translatedMessage
+                        ? error?.translatedMessage
+                        : error?.message}
+                    </div>
+                    {error?.tryAgain ? `${t('try_again')}.` : null}
+                  </>
+                }
+              />
             )}
           </Fragment>
         )

+ 9 - 7
services/web/frontend/js/features/project-list/components/notifications/notification.tsx

@@ -61,13 +61,15 @@ function Notification({
       bsStyle === 'danger' ? 'error' : bsStyle
     ) as NotificationType
     return (
-      <NewNotification
-        type={newNotificationType}
-        isDismissible={onDismiss != null}
-        onDismiss={handleDismiss}
-        content={body as React.ReactElement}
-        action={action}
-      />
+      <li>
+        <NewNotification
+          type={newNotificationType}
+          isDismissible={onDismiss != null}
+          onDismiss={handleDismiss}
+          content={body as React.ReactElement}
+          action={action}
+        />
+      </li>
     )
   }
 

+ 13 - 4
services/web/frontend/js/features/project-list/components/notifications/user-notifications.tsx

@@ -13,6 +13,7 @@ import getMeta from '../../../../utils/meta'
 import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
 import customLocalStorage from '../../../../infrastructure/local-storage'
 import { sendMB } from '../../../../infrastructure/event-tracking'
+import classNames from 'classnames'
 import { isSplitTestEnabled } from '@/utils/splitTestUtils'
 
 const isChromium = () =>
@@ -34,6 +35,10 @@ const EnrollmentNotification: JSXElementConstructor<{
 }> = enrollmentNotificationModule?.import.default
 
 function UserNotifications() {
+  const newNotificationStyle = getMeta(
+    'ol-newNotificationStyle',
+    false
+  ) as boolean
   const groupSubscriptionsPendingEnrollment: Subscription[] = getMeta(
     'ol-groupSubscriptionsPendingEnrollment',
     []
@@ -61,7 +66,7 @@ function UserNotifications() {
     }
 
     const show =
-      user.writefull?.enabled === true || // show to any users who have writefull enabled regardless of split test
+      user?.writefull?.enabled === true || // show to any users who have writefull enabled regardless of split test
       (!writefullIntegrationSplitTestEnabled && // show old banner to users who are not in the split test, who are on chrome and havent dismissed
         isChromium() &&
         getMeta('ol-showWritefullPromoBanner'))
@@ -71,7 +76,7 @@ function UserNotifications() {
         location: 'dashboard-banner',
         page: '/project',
         name:
-          user.writefull?.enabled === true ||
+          user?.writefull?.enabled === true ||
           writefullIntegrationSplitTestEnabled
             ? 'writefull-premium'
             : 'writefull',
@@ -83,7 +88,11 @@ function UserNotifications() {
   const [dismissedWritefull, setDismissedWritefull] = useState(false)
 
   return (
-    <div className="user-notifications">
+    <div
+      className={classNames('user-notifications', {
+        'notification-list': newNotificationStyle,
+      })}
+    >
       <ul className="list-unstyled">
         {EnrollmentNotification &&
           groupSubscriptionsPendingEnrollment.map(subscription => (
@@ -110,7 +119,7 @@ function UserNotifications() {
             splitTestName={inrGeoBannerSplitTestName}
           />
         ) : null}
-        {writefullIntegrationSplitTestEnabled || user.writefull?.enabled ? (
+        {writefullIntegrationSplitTestEnabled || user?.writefull?.enabled ? (
           <WritefullPremiumPromoBanner
             show={showWritefull}
             setShow={setShowWritefull}

+ 1 - 1
services/web/frontend/js/features/project-list/components/notifications/writefull-premium-promo-banner.tsx

@@ -44,7 +44,7 @@ function WritefullPremiumPromoBanner({
       }
       action={
         <a
-          className="btn btn-secondary"
+          className="btn btn-secondary btn-sm"
           href="https://my.writefull.com/overleaf-invite?code=OVERLEAF10&redirect=plans"
           target="_blank"
           rel="noreferrer"

+ 10 - 1
services/web/frontend/js/features/project-list/components/notifications/writefull-promo-banner.tsx

@@ -2,6 +2,7 @@ import { memo, useCallback } from 'react'
 import Notification from './notification'
 import { sendMB } from '../../../../infrastructure/event-tracking'
 import customLocalStorage from '../../../../infrastructure/local-storage'
+import getMeta from '@/utils/meta'
 
 const eventSegmentation = {
   location: 'dashboard-banner',
@@ -18,6 +19,10 @@ function WritefullPromoBanner({
   setShow: (value: boolean) => void
   onDismiss: () => void
 }) {
+  const newNotificationStyle = getMeta(
+    'ol-newNotificationStyle',
+    false
+  ) as boolean
   const handleOpenLink = useCallback(() => {
     sendMB('promo-click', eventSegmentation)
   }, [])
@@ -47,7 +52,11 @@ function WritefullPromoBanner({
       }
       action={
         <a
-          className="pull-right btn btn-info btn-sm"
+          className={
+            newNotificationStyle
+              ? 'btn btn-secondary btn-sm'
+              : 'pull-right btn btn-info btn-sm'
+          }
           href="https://my.writefull.com/overleaf-invite?code=OVERLEAF10"
           target="_blank"
           rel="noreferrer"

+ 340 - 0
services/web/frontend/stories/project-list/notifications-new.stories.tsx

@@ -0,0 +1,340 @@
+import UserNotifications from '../../js/features/project-list/components/notifications/user-notifications'
+import { ProjectListProvider } from '../../js/features/project-list/context/project-list-context'
+import useFetchMock from '../hooks/use-fetch-mock'
+import {
+  commonSetupMocks,
+  errorsMocks,
+  fakeReconfirmationUsersData,
+  institutionSetupMocks,
+  reconfirmAffiliationSetupMocks,
+  reconfirmationSetupMocks,
+  setCommonMeta,
+  setInstitutionMeta,
+  setReconfirmAffiliationMeta,
+  setReconfirmationMeta,
+} from './helpers/emails'
+
+export const ProjectInvite = (args: any) => {
+  useFetchMock(commonSetupMocks)
+  setCommonMeta({
+    templateKey: 'notification_project_invite',
+    messageOpts: {
+      projectId: '123',
+      projectName: 'Abc Project',
+      userName: 'fakeUser',
+      token: 'abcdef',
+    },
+  })
+  window.metaAttributesCache.set('ol-newNotificationStyle', true)
+  return (
+    <ProjectListProvider>
+      <UserNotifications {...args} />
+    </ProjectListProvider>
+  )
+}
+
+export const ProjectInviteNetworkError = (args: any) => {
+  useFetchMock(errorsMocks)
+  setCommonMeta({
+    templateKey: 'notification_project_invite',
+    messageOpts: {
+      projectId: '123',
+      projectName: 'Abc Project',
+      userName: 'fakeUser',
+      token: 'abcdef',
+    },
+  })
+  window.metaAttributesCache.set('ol-newNotificationStyle', true)
+  return (
+    <ProjectListProvider>
+      <UserNotifications {...args} />
+    </ProjectListProvider>
+  )
+}
+
+export const Wfh2020UpgradeOffer = (args: any) => {
+  useFetchMock(commonSetupMocks)
+  setCommonMeta({
+    _id: 1,
+    templateKey: 'wfh_2020_upgrade_offer',
+  })
+  window.metaAttributesCache.set('ol-newNotificationStyle', true)
+  return (
+    <ProjectListProvider>
+      <UserNotifications {...args} />
+    </ProjectListProvider>
+  )
+}
+
+export const IPMatchedAffiliationSsoEnabled = (args: any) => {
+  useFetchMock(commonSetupMocks)
+  setCommonMeta({
+    _id: 1,
+    templateKey: 'notification_ip_matched_affiliation',
+    messageOpts: {
+      university_name: 'Abc University',
+      institutionId: '456',
+      ssoEnabled: true,
+    },
+  })
+  window.metaAttributesCache.set('ol-newNotificationStyle', true)
+  return (
+    <ProjectListProvider>
+      <UserNotifications {...args} />
+    </ProjectListProvider>
+  )
+}
+
+export const IPMatchedAffiliationSsoDisabled = (args: any) => {
+  useFetchMock(commonSetupMocks)
+  setCommonMeta({
+    _id: 1,
+    templateKey: 'notification_ip_matched_affiliation',
+    messageOpts: {
+      university_name: 'Abc University',
+      institutionId: '456',
+      ssoEnabled: false,
+    },
+  })
+  window.metaAttributesCache.set('ol-newNotificationStyle', true)
+  return (
+    <ProjectListProvider>
+      <UserNotifications {...args} />
+    </ProjectListProvider>
+  )
+}
+
+export const TpdsFileLimit = (args: any) => {
+  useFetchMock(commonSetupMocks)
+  setCommonMeta({
+    _id: 1,
+    templateKey: 'notification_tpds_file_limit',
+    messageOpts: {
+      projectName: 'Abc Project',
+    },
+  })
+  window.metaAttributesCache.set('ol-newNotificationStyle', true)
+  return (
+    <ProjectListProvider>
+      <UserNotifications {...args} />
+    </ProjectListProvider>
+  )
+}
+
+export const DropBoxDuplicateProjectNames = (args: any) => {
+  useFetchMock(commonSetupMocks)
+  setCommonMeta({
+    _id: 1,
+    templateKey: 'notification_dropbox_duplicate_project_names',
+    messageOpts: {
+      projectName: 'Abc Project',
+    },
+  })
+  window.metaAttributesCache.set('ol-newNotificationStyle', true)
+  return (
+    <ProjectListProvider>
+      <UserNotifications {...args} />
+    </ProjectListProvider>
+  )
+}
+
+export const DropBoxUnlinkedDueToLapsedReconfirmation = (args: any) => {
+  useFetchMock(commonSetupMocks)
+  setCommonMeta({
+    _id: 1,
+    templateKey: 'notification_dropbox_unlinked_due_to_lapsed_reconfirmation',
+  })
+  window.metaAttributesCache.set('ol-newNotificationStyle', true)
+  return (
+    <ProjectListProvider>
+      <UserNotifications {...args} />
+    </ProjectListProvider>
+  )
+}
+
+export const NotificationGroupInvitation = (args: any) => {
+  useFetchMock(commonSetupMocks)
+  setCommonMeta({
+    _id: 1,
+    templateKey: 'notification_group_invitation',
+    messageOpts: {
+      inviterName: 'John Doe',
+    },
+  })
+  window.metaAttributesCache.set('ol-newNotificationStyle', true)
+  return (
+    <ProjectListProvider>
+      <UserNotifications {...args} />
+    </ProjectListProvider>
+  )
+}
+
+export const NotificationGroupInvitationCancelSubscription = (args: any) => {
+  useFetchMock(commonSetupMocks)
+  setCommonMeta({
+    _id: 1,
+    templateKey: 'notification_group_invitation',
+    messageOpts: {
+      inviterName: 'John Doe',
+    },
+  })
+  window.metaAttributesCache.set('ol-newNotificationStyle', true)
+  window.metaAttributesCache.set('ol-hasIndividualRecurlySubscription', true)
+  return (
+    <ProjectListProvider>
+      <UserNotifications {...args} />
+    </ProjectListProvider>
+  )
+}
+
+export const NonSpecificMessage = (args: any) => {
+  useFetchMock(commonSetupMocks)
+  setCommonMeta({ _id: 1, html: 'Non specific message' })
+  window.metaAttributesCache.set('ol-newNotificationStyle', true)
+  return (
+    <ProjectListProvider>
+      <UserNotifications {...args} />
+    </ProjectListProvider>
+  )
+}
+
+export const InstitutionSsoAvailable = (args: any) => {
+  useFetchMock(institutionSetupMocks)
+  setInstitutionMeta({
+    _id: 1,
+    templateKey: 'notification_institution_sso_available',
+  })
+  window.metaAttributesCache.set('ol-newNotificationStyle', true)
+  return (
+    <ProjectListProvider>
+      <UserNotifications {...args} />
+    </ProjectListProvider>
+  )
+}
+
+export const InstitutionSsoLinked = (args: any) => {
+  useFetchMock(institutionSetupMocks)
+  setInstitutionMeta({
+    _id: 1,
+    templateKey: 'notification_institution_sso_linked',
+  })
+  window.metaAttributesCache.set('ol-newNotificationStyle', true)
+  return (
+    <ProjectListProvider>
+      <UserNotifications {...args} />
+    </ProjectListProvider>
+  )
+}
+
+export const InstitutionSsoNonCanonical = (args: any) => {
+  useFetchMock(institutionSetupMocks)
+  setInstitutionMeta({
+    _id: 1,
+    templateKey: 'notification_institution_sso_non_canonical',
+  })
+  window.metaAttributesCache.set('ol-newNotificationStyle', true)
+  return (
+    <ProjectListProvider>
+      <UserNotifications {...args} />
+    </ProjectListProvider>
+  )
+}
+
+export const InstitutionSsoAlreadyRegistered = (args: any) => {
+  useFetchMock(institutionSetupMocks)
+  setInstitutionMeta({
+    _id: 1,
+    templateKey: 'notification_institution_sso_already_registered',
+  })
+  window.metaAttributesCache.set('ol-newNotificationStyle', true)
+  return (
+    <ProjectListProvider>
+      <UserNotifications {...args} />
+    </ProjectListProvider>
+  )
+}
+
+export const InstitutionSsoError = (args: any) => {
+  useFetchMock(institutionSetupMocks)
+  setInstitutionMeta({
+    templateKey: 'notification_institution_sso_error',
+    error: {
+      message: 'message',
+      translatedMessage: 'Translated Message',
+      tryAgain: true,
+    },
+  })
+  window.metaAttributesCache.set('ol-newNotificationStyle', true)
+  return (
+    <ProjectListProvider>
+      <UserNotifications {...args} />
+    </ProjectListProvider>
+  )
+}
+
+export const ResendConfirmationEmail = (args: any) => {
+  useFetchMock(reconfirmationSetupMocks)
+  setReconfirmationMeta()
+  window.metaAttributesCache.set('ol-newNotificationStyle', true)
+  return (
+    <ProjectListProvider>
+      <UserNotifications {...args} />
+    </ProjectListProvider>
+  )
+}
+
+export const ResendConfirmationEmailNetworkError = (args: any) => {
+  useFetchMock(errorsMocks)
+  setReconfirmationMeta()
+  window.metaAttributesCache.set('ol-newNotificationStyle', true)
+  return (
+    <ProjectListProvider>
+      <UserNotifications {...args} />
+    </ProjectListProvider>
+  )
+}
+
+export const ReconfirmAffiliation = (args: any) => {
+  useFetchMock(reconfirmAffiliationSetupMocks)
+  setReconfirmAffiliationMeta()
+  window.metaAttributesCache.set('ol-allInReconfirmNotificationPeriods', [
+    fakeReconfirmationUsersData,
+  ])
+  window.metaAttributesCache.set('ol-newNotificationStyle', true)
+  return (
+    <ProjectListProvider>
+      <UserNotifications {...args} />
+    </ProjectListProvider>
+  )
+}
+
+export const ReconfirmAffiliationNetworkError = (args: any) => {
+  useFetchMock(errorsMocks)
+  setReconfirmAffiliationMeta()
+  window.metaAttributesCache.set('ol-allInReconfirmNotificationPeriods', [
+    fakeReconfirmationUsersData,
+  ])
+  window.metaAttributesCache.set('ol-newNotificationStyle', true)
+  return (
+    <ProjectListProvider>
+      <UserNotifications {...args} />
+    </ProjectListProvider>
+  )
+}
+
+export const ReconfirmedAffiliationSuccess = (args: any) => {
+  useFetchMock(reconfirmAffiliationSetupMocks)
+  setReconfirmAffiliationMeta()
+  window.metaAttributesCache.set('ol-userEmails', [fakeReconfirmationUsersData])
+  window.metaAttributesCache.set('ol-newNotificationStyle', true)
+  return (
+    <ProjectListProvider>
+      <UserNotifications {...args} />
+    </ProjectListProvider>
+  )
+}
+
+export default {
+  title: 'Project List / Notifications New Style',
+  component: UserNotifications,
+}