mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-14 14:13:43 +01:00
Show dashboard notification for unconfirmed emails and untrusted secondary emails (#23919)
* Show an aggressive dashboard notification for unconfirmed emails Show a persistent dashboard notification for untrusted secondary emails * For emails before the cutoffDate, start displaying the notification on the deletionDate and show the notification for 90 days * Update the email deletion logic for displaying the email notification and update test * Update test GitOrigin-RevId: 1b0e44f79592292d428c634dc1ec4df9e6ceaeb4
This commit is contained in:
+171
-43
@@ -1,6 +1,5 @@
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import Notification from '../notification'
|
||||
import Icon from '../../../../../shared/components/icon'
|
||||
import getMeta from '../../../../../utils/meta'
|
||||
import useAsync from '../../../../../shared/hooks/use-async'
|
||||
import { useProjectListContext } from '../../../context/project-list-context'
|
||||
@@ -11,6 +10,7 @@ import {
|
||||
import { UserEmailData } from '../../../../../../../types/user-email'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
import LoadingSpinner from '@/shared/components/loading-spinner'
|
||||
|
||||
const ssoAvailable = ({ samlProviderId, affiliation }: UserEmailData) => {
|
||||
const { hasSamlFeature, hasSamlBeta } = getMeta('ol-ExposedSettings')
|
||||
@@ -69,17 +69,71 @@ function isOnFreeOrIndividualPlan() {
|
||||
const showConfirmEmail = (userEmail: UserEmailData) => {
|
||||
const { emailConfirmationDisabled } = getMeta('ol-ExposedSettings')
|
||||
|
||||
return (
|
||||
!emailConfirmationDisabled &&
|
||||
!userEmail.confirmedAt &&
|
||||
!ssoAvailable(userEmail)
|
||||
)
|
||||
return !emailConfirmationDisabled && !ssoAvailable(userEmail)
|
||||
}
|
||||
|
||||
function ConfirmEmailNotification({ userEmail }: { userEmail: UserEmailData }) {
|
||||
const EMAIL_DELETION_SCHEDULE = {
|
||||
'2025-06-03': '2017-12-31',
|
||||
'2025-07-03': '2019-12-31',
|
||||
'2025-08-03': '2021-12-31',
|
||||
'2025-09-03': '2025-03-03',
|
||||
}
|
||||
|
||||
// Emails that remain unconfirmed after 90 days will be removed from the account
|
||||
function getEmailDeletionDate(emailData: UserEmailData, signUpDate: string) {
|
||||
if (emailData.default) return false
|
||||
if (emailData.confirmedAt) return false
|
||||
|
||||
if (!signUpDate) return false
|
||||
|
||||
const currentDate = new Date()
|
||||
|
||||
for (const [deletionDate, cutoffDate] of Object.entries(
|
||||
EMAIL_DELETION_SCHEDULE
|
||||
)) {
|
||||
const emailSignupDate = new Date(signUpDate)
|
||||
const emailCutoffDate = new Date(cutoffDate)
|
||||
const emailDeletionDate = new Date(deletionDate)
|
||||
|
||||
if (emailSignupDate < emailCutoffDate) {
|
||||
const notificationStartDate = new Date(
|
||||
emailDeletionDate.getTime() - 90 * 24 * 60 * 60 * 1000
|
||||
)
|
||||
if (currentDate >= notificationStartDate) {
|
||||
if (currentDate > emailDeletionDate) {
|
||||
return new Date().toLocaleDateString()
|
||||
}
|
||||
return emailDeletionDate.toLocaleDateString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function ConfirmEmailNotification({
|
||||
userEmail,
|
||||
signUpDate,
|
||||
}: {
|
||||
userEmail: UserEmailData
|
||||
signUpDate: string
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const { isLoading, isSuccess, isError, error, runAsync } = useAsync()
|
||||
|
||||
// We consider secondary emails added on or after 22.03.2024 to be trusted for account recovery
|
||||
// https://github.com/overleaf/internal/pull/17572
|
||||
const emailTrustCutoffDate = new Date('2024-03-22')
|
||||
const emailDeletionDate = getEmailDeletionDate(userEmail, signUpDate)
|
||||
const isPrimary = userEmail.default
|
||||
|
||||
const isEmailTrusted =
|
||||
userEmail.lastConfirmedAt &&
|
||||
new Date(userEmail.lastConfirmedAt) >= emailTrustCutoffDate
|
||||
|
||||
const shouldShowCommonsNotification =
|
||||
emailHasLicenceAfterConfirming(userEmail) && isOnFreeOrIndividualPlan()
|
||||
|
||||
const handleResendConfirmationEmail = ({ email }: UserEmailData) => {
|
||||
runAsync(
|
||||
postJSON('/user/emails/resend_confirmation', {
|
||||
@@ -92,20 +146,120 @@ function ConfirmEmailNotification({ userEmail }: { userEmail: UserEmailData }) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!userEmail.lastConfirmedAt && !shouldShowCommonsNotification) {
|
||||
return (
|
||||
<Notification
|
||||
type="warning"
|
||||
content={
|
||||
<div data-testid="pro-notification-body">
|
||||
{isLoading ? (
|
||||
<div data-testid="loading-resending-confirmation-email">
|
||||
<LoadingSpinner
|
||||
loadingText={t('resending_confirmation_email')}
|
||||
/>
|
||||
</div>
|
||||
) : isError ? (
|
||||
<div aria-live="polite">{getUserFacingMessage(error)}</div>
|
||||
) : (
|
||||
<>
|
||||
<p>
|
||||
{isPrimary
|
||||
? t('please_confirm_primary_email', {
|
||||
emailAddress: userEmail.email,
|
||||
})
|
||||
: t('please_confirm_secondary_email', {
|
||||
emailAddress: userEmail.email,
|
||||
})}
|
||||
</p>
|
||||
{emailDeletionDate && (
|
||||
<p>
|
||||
{t('email_remove_by_date', { date: emailDeletionDate })}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
action={
|
||||
<>
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
onClick={() => handleResendConfirmationEmail(userEmail)}
|
||||
>
|
||||
{t('resend_confirmation_email')}
|
||||
</OLButton>
|
||||
<OLButton variant="link" href="/user/settings">
|
||||
{isPrimary
|
||||
? t('change_primary_email')
|
||||
: t('remove_email_address')}
|
||||
</OLButton>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (!isEmailTrusted && !isPrimary && !shouldShowCommonsNotification) {
|
||||
return (
|
||||
<Notification
|
||||
type="warning"
|
||||
content={
|
||||
<div data-testid="not-trusted-notification-body">
|
||||
{isLoading ? (
|
||||
<div data-testid="error-id">
|
||||
<LoadingSpinner
|
||||
loadingText={t('resending_confirmation_email')}
|
||||
/>
|
||||
</div>
|
||||
) : isError ? (
|
||||
<div aria-live="polite">{getUserFacingMessage(error)}</div>
|
||||
) : (
|
||||
<>
|
||||
<p>
|
||||
<b>{t('confirm_secondary_email')}</b>
|
||||
</p>
|
||||
<p>
|
||||
{t('reconfirm_secondary_email', {
|
||||
emailAddress: userEmail.email,
|
||||
})}
|
||||
</p>
|
||||
<p>{t('ensure_recover_account')}</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
action={
|
||||
<>
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
onClick={() => handleResendConfirmationEmail(userEmail)}
|
||||
>
|
||||
{t('resend_confirmation_email')}
|
||||
</OLButton>
|
||||
<OLButton
|
||||
variant="link"
|
||||
href="/user/settings"
|
||||
style={{ textDecoration: 'underline' }}
|
||||
>
|
||||
{t('remove_email_address')}
|
||||
</OLButton>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// Only show the notification if a) a commons license is available and b) the
|
||||
// user is on a free or individual plan. Users on a group or Commons plan
|
||||
// already have premium features.
|
||||
if (emailHasLicenceAfterConfirming(userEmail) && isOnFreeOrIndividualPlan()) {
|
||||
if (shouldShowCommonsNotification) {
|
||||
return (
|
||||
<Notification
|
||||
type="info"
|
||||
content={
|
||||
<div data-testid="notification-body">
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Icon type="spinner" spin /> {t('resending_confirmation_email')}
|
||||
…
|
||||
</>
|
||||
<LoadingSpinner loadingText={t('resending_confirmation_email')} />
|
||||
) : isError ? (
|
||||
<div aria-live="polite">{getUserFacingMessage(error)}</div>
|
||||
) : (
|
||||
@@ -141,43 +295,15 @@ function ConfirmEmailNotification({ userEmail }: { userEmail: UserEmailData }) {
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Notification
|
||||
type="warning"
|
||||
content={
|
||||
<div data-testid="pro-notification-body">
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Icon type="spinner" spin /> {t('resending_confirmation_email')}
|
||||
…
|
||||
</>
|
||||
) : isError ? (
|
||||
<div aria-live="polite">{getUserFacingMessage(error)}</div>
|
||||
) : (
|
||||
<>
|
||||
{t('please_confirm_email', {
|
||||
emailAddress: userEmail.email,
|
||||
})}{' '}
|
||||
<OLButton
|
||||
variant="link"
|
||||
onClick={() => handleResendConfirmationEmail(userEmail)}
|
||||
className="btn-inline-link"
|
||||
>
|
||||
{t('resend_confirmation_email')}
|
||||
</OLButton>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
function ConfirmEmail() {
|
||||
const { totalProjectsCount } = useProjectListContext()
|
||||
const userEmails = getMeta('ol-userEmails') || []
|
||||
const signUpDate = getMeta('ol-user')?.signUpDate
|
||||
|
||||
if (!totalProjectsCount || !userEmails.length) {
|
||||
if (!totalProjectsCount || !userEmails.length || !signUpDate) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -188,6 +314,7 @@ function ConfirmEmail() {
|
||||
<ConfirmEmailNotification
|
||||
key={`confirm-email-${userEmail.email}`}
|
||||
userEmail={userEmail}
|
||||
signUpDate={signUpDate}
|
||||
/>
|
||||
) : null
|
||||
})}
|
||||
@@ -196,3 +323,4 @@ function ConfirmEmail() {
|
||||
}
|
||||
|
||||
export default ConfirmEmail
|
||||
export { getEmailDeletionDate }
|
||||
|
||||
Reference in New Issue
Block a user