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:
@@ -305,6 +305,7 @@
|
||||
"confirm_reject_selected_changes": "",
|
||||
"confirm_reject_selected_changes_plural": "",
|
||||
"confirm_remove_sso_config_enter_email": "",
|
||||
"confirm_secondary_email": "",
|
||||
"confirm_your_email": "",
|
||||
"confirming": "",
|
||||
"conflicting_paths_found": "",
|
||||
@@ -488,6 +489,7 @@
|
||||
"email_link_expired": "",
|
||||
"email_must_be_linked_to_institution": "",
|
||||
"email_or_password_wrong_try_again": "",
|
||||
"email_remove_by_date": "",
|
||||
"emails_and_affiliations_explanation": "",
|
||||
"emails_and_affiliations_title": "",
|
||||
"empty": "",
|
||||
@@ -502,6 +504,7 @@
|
||||
"enables_real_time_syntax_checking_in_the_editor": "",
|
||||
"enabling": "",
|
||||
"end_of_document": "",
|
||||
"ensure_recover_account": "",
|
||||
"enter_6_digit_code": "",
|
||||
"enter_any_size_including_units_or_valid_latex_command": "",
|
||||
"enter_image_url": "",
|
||||
@@ -1185,7 +1188,8 @@
|
||||
"please_check_your_inbox_to_confirm": "",
|
||||
"please_compile_pdf_before_download": "",
|
||||
"please_compile_pdf_before_word_count": "",
|
||||
"please_confirm_email": "",
|
||||
"please_confirm_primary_email": "",
|
||||
"please_confirm_secondary_email": "",
|
||||
"please_confirm_your_email_before_making_it_default": "",
|
||||
"please_contact_support_to_makes_change_to_your_plan": "",
|
||||
"please_enter_confirmation_code": "",
|
||||
@@ -1300,6 +1304,7 @@
|
||||
"recompile": "",
|
||||
"recompile_from_scratch": "",
|
||||
"recompile_pdf": "",
|
||||
"reconfirm_secondary_email": "",
|
||||
"reconnect": "",
|
||||
"reconnecting": "",
|
||||
"reconnecting_in_x_secs": "",
|
||||
@@ -1333,6 +1338,7 @@
|
||||
"remove": "",
|
||||
"remove_access": "",
|
||||
"remove_add_on": "",
|
||||
"remove_email_address": "",
|
||||
"remove_from_group": "",
|
||||
"remove_link": "",
|
||||
"remove_manager": "",
|
||||
|
||||
+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