[web] Use 6-digits verification in project-list notifications (bis) (#25847)

* Pull email context outside of `ResendConfirmationCodeModal`

* Use `loading` prop of button instead of deprecated Icon

* Swap notification order to clarify priority (no change in behaviour)

* Replace confirmation link action by confirmationCodeModal, and simplify code

* Change to secondary button variant in the Notification

* Display errors within the modal

* Increase ratelimit for resend-confirmation

* Copy changes

* Add stories on email confirmation notifications

* Fix other Notification stories

* Update tests

* Update services/web/frontend/js/features/settings/components/emails/confirm-email-form.tsx

Co-authored-by: Rebeka Dekany <50901361+rebekadekany@users.noreply.github.com>

* Remove placeholder on 6-digit code input

---------

Co-authored-by: Rebeka Dekany <50901361+rebekadekany@users.noreply.github.com>
GitOrigin-RevId: dad8bfd79505a2e7d065fd48791fd57c8a31e071
This commit is contained in:
Antoine Clausse
2025-05-27 10:02:13 +02:00
committed by Copybot
parent 2c07e1c44a
commit 2ce9b399cb
12 changed files with 282 additions and 265 deletions
+1 -1
View File
@@ -182,7 +182,7 @@ const rateLimiters = {
duration: 60,
}),
sendConfirmation: new RateLimiter('send-confirmation', {
points: 1,
points: 2,
duration: 60,
}),
sendChatMessage: new RateLimiter('send-chat-message', {
@@ -519,7 +519,6 @@
"enabling": "",
"end_of_document": "",
"ensure_recover_account": "",
"enter_6_digit_code": "",
"enter_any_size_including_units_or_valid_latex_command": "",
"enter_image_url": "",
"enter_the_code": "",
@@ -1224,8 +1223,8 @@
"please_check_your_inbox_to_confirm": "",
"please_compile_pdf_before_download": "",
"please_compile_pdf_before_word_count": "",
"please_confirm_primary_email": "",
"please_confirm_secondary_email": "",
"please_confirm_primary_email_or_edit": "",
"please_confirm_secondary_email_or_edit": "",
"please_confirm_your_email_before_making_it_default": "",
"please_contact_support_to_makes_change_to_your_plan": "",
"please_enter_confirmation_code": "",
@@ -1375,7 +1374,6 @@
"remote_service_error": "",
"remove": "",
"remove_access": "",
"remove_email_address": "",
"remove_from_group": "",
"remove_link": "",
"remove_manager": "",
@@ -1408,7 +1406,6 @@
"resend_link_sso": "",
"resend_managed_user_invite": "",
"resending_confirmation_code": "",
"resending_confirmation_email": "",
"resize": "",
"resolve_comment": "",
"resolve_comment_error_message": "",
@@ -1520,6 +1517,7 @@
"select_user": "",
"selected": "",
"selection_deleted": "",
"send_confirmation_code": "",
"send_first_message": "",
"send_message": "",
"send_request": "",
@@ -1,16 +1,10 @@
import { Trans, useTranslation } from 'react-i18next'
import Notification from '../notification'
import getMeta from '../../../../../utils/meta'
import useAsync from '../../../../../shared/hooks/use-async'
import { useProjectListContext } from '../../../context/project-list-context'
import {
postJSON,
getUserFacingMessage,
} from '../../../../../infrastructure/fetch-json'
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'
import ResendConfirmationCodeModal from '@/features/settings/components/emails/resend-confirmation-code-modal'
import { ReactNode, useState } from 'react'
const ssoAvailable = ({ samlProviderId, affiliation }: UserEmailData) => {
const { hasSamlFeature, hasSamlBeta } = getMeta('ol-ExposedSettings')
@@ -114,12 +108,17 @@ function getEmailDeletionDate(emailData: UserEmailData, signUpDate: string) {
function ConfirmEmailNotification({
userEmail,
signUpDate,
setIsLoading,
isLoading,
}: {
userEmail: UserEmailData
signUpDate: string
setIsLoading: (loading: boolean) => void
isLoading: boolean
}) {
const { t } = useTranslation()
const { isLoading, isSuccess, isError, error, runAsync } = useAsync()
const [isSuccess, setIsSuccess] = useState(false)
const emailAddress = userEmail.email
// We consider secondary emails added on or after 22.03.2024 to be trusted for account recovery
// https://github.com/overleaf/internal/pull/17572
@@ -127,6 +126,7 @@ function ConfirmEmailNotification({
const emailDeletionDate = getEmailDeletionDate(userEmail, signUpDate)
const isPrimary = userEmail.default
const isEmailConfirmed = !!userEmail.lastConfirmedAt
const isEmailTrusted =
userEmail.lastConfirmedAt &&
new Date(userEmail.lastConfirmedAt) >= emailTrustCutoffDate
@@ -134,163 +134,97 @@ function ConfirmEmailNotification({
const shouldShowCommonsNotification =
emailHasLicenceAfterConfirming(userEmail) && isOnFreeOrIndividualPlan()
const handleResendConfirmationEmail = ({ email }: UserEmailData) => {
runAsync(
postJSON('/user/emails/resend_confirmation', {
body: { email },
})
).catch(debugConsole.error)
}
if (isSuccess) {
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>
</>
}
/>
)
}
const confirmationCodeModal = (
<ResendConfirmationCodeModal
email={emailAddress}
onSuccess={() => setIsSuccess(true)}
setGroupLoading={setIsLoading}
groupLoading={isLoading}
triggerVariant="secondary"
/>
)
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>
</>
}
/>
)
}
let notificationType: 'info' | 'warning' | undefined
let notificationBody: ReactNode | undefined
// 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 (shouldShowCommonsNotification) {
notificationType = 'info'
notificationBody = (
<>
<Trans
i18nKey="one_step_away_from_professional_features"
components={[<strong />]} // eslint-disable-line react/jsx-key
/>
<br />
<Trans
i18nKey="institution_has_overleaf_subscription"
values={{
institutionName: userEmail.affiliation?.institution.name,
emailAddress,
}}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
components={[<strong />]} // eslint-disable-line react/jsx-key
/>
</>
)
} else if (!isEmailConfirmed) {
notificationType = 'warning'
notificationBody = (
<>
<p>
{isPrimary ? (
<Trans
i18nKey="please_confirm_primary_email_or_edit"
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
values={{ emailAddress }}
components={[
// eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key
<a href="/user/settings" />,
]}
/>
) : (
<Trans
i18nKey="please_confirm_secondary_email_or_edit"
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
values={{ emailAddress }}
components={[
// eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key
<a href="/user/settings" />,
]}
/>
)}
</p>
{emailDeletionDate && (
<p>{t('email_remove_by_date', { date: emailDeletionDate })}</p>
)}
</>
)
} else if (!isEmailTrusted && !isPrimary) {
notificationType = 'warning'
notificationBody = (
<>
<p>
<b>{t('confirm_secondary_email')}</b>
</p>
<p>{t('reconfirm_secondary_email', { emailAddress })}</p>
<p>{t('ensure_recover_account')}</p>
</>
)
}
if (notificationType) {
return (
<Notification
type="info"
content={
<div data-testid="notification-body">
{isLoading ? (
<LoadingSpinner loadingText={t('resending_confirmation_email')} />
) : isError ? (
<div aria-live="polite">{getUserFacingMessage(error)}</div>
) : (
<>
<Trans
i18nKey="one_step_away_from_professional_features"
components={[<strong />]} // eslint-disable-line react/jsx-key
/>
<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>
}
action={
<OLButton
variant="secondary"
onClick={() => handleResendConfirmationEmail(userEmail)}
>
{t('resend_email')}
</OLButton>
}
type={notificationType}
content={notificationBody}
action={confirmationCodeModal}
/>
)
}
@@ -302,6 +236,7 @@ function ConfirmEmail() {
const { totalProjectsCount } = useProjectListContext()
const userEmails = getMeta('ol-userEmails') || []
const signUpDate = getMeta('ol-user')?.signUpDate
const [isLoading, setIsLoading] = useState(false)
if (!totalProjectsCount || !userEmails.length || !signUpDate) {
return null
@@ -315,6 +250,8 @@ function ConfirmEmail() {
key={`confirm-email-${userEmail.email}`}
userEmail={userEmail}
signUpDate={signUpDate}
isLoading={isLoading}
setIsLoading={setIsLoading}
/>
) : null
})}
@@ -2,7 +2,7 @@ import { postJSON } from '@/infrastructure/fetch-json'
import useWaitForI18n from '@/shared/hooks/use-wait-for-i18n'
import Notification from '@/shared/components/notification'
import getMeta from '@/utils/meta'
import { FormEvent, MouseEventHandler, useState } from 'react'
import { FormEvent, MouseEventHandler, ReactNode, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import LoadingSpinner from '@/shared/components/loading-spinner'
import MaterialIcon from '@/shared/components/material-icon'
@@ -27,6 +27,7 @@ type ConfirmEmailFormProps = {
interstitial: boolean
isModal?: boolean
onCancel?: () => void
outerError?: string
}
export function ConfirmEmailForm({
@@ -40,15 +41,17 @@ export function ConfirmEmailForm({
interstitial,
isModal,
onCancel,
outerError,
}: ConfirmEmailFormProps) {
const { t } = useTranslation()
const [confirmationCode, setConfirmationCode] = useState('')
const [feedback, setFeedback] = useState<Feedback | null>(null)
const [isConfirming, setIsConfirming] = useState(false)
const [isResending, setIsResending] = useState(false)
const [hasResent, setHasResent] = useState(false)
const [successRedirectPath, setSuccessRedirectPath] = useState('')
const { isReady } = useWaitForI18n()
const outerErrorDisplay = (!hasResent && outerError) || null
const errorHandler = (err: any, actionType?: string) => {
let errorName = err?.data?.message?.key || 'generic_something_went_wrong'
@@ -131,6 +134,7 @@ export function ConfirmEmailForm({
})
.finally(() => {
setIsResending(false)
setHasResent(true)
})
sendMB('email-verification-click', {
@@ -158,8 +162,15 @@ export function ConfirmEmailForm({
)
}
let intro = <h5 className="h5">{t('confirm_your_email')}</h5>
if (isModal) intro = <h5 className="h5">{t('we_sent_code')}</h5>
let intro: ReactNode | null = (
<h5 className="h5">{t('confirm_your_email')}</h5>
)
if (isModal)
intro = outerErrorDisplay ? (
<div className="mt-4" />
) : (
<h3 className="h5">{outerErrorDisplay ? null : t('we_sent_code')}</h3>
)
if (interstitial)
intro = (
<h1 className="h3 interstitial-header">{t('confirm_your_email')}</h1>
@@ -172,12 +183,14 @@ export function ConfirmEmailForm({
className="confirm-email-form"
>
<div className="confirm-email-form-inner">
{feedback?.type === 'alert' && (
{(feedback?.type === 'alert' || outerErrorDisplay) && (
<Notification
ariaLive="polite"
className="confirm-email-alert"
type={feedback.style}
content={<ErrorMessage error={feedback.message} />}
type={outerErrorDisplay ? 'error' : feedback!.style}
content={
outerErrorDisplay || <ErrorMessage error={feedback!.message!} />
}
/>
)}
@@ -191,7 +204,6 @@ export function ConfirmEmailForm({
<input
id="one-time-code"
className="form-control"
placeholder={t('enter_6_digit_code')}
inputMode="numeric"
required
value={confirmationCode}
@@ -200,6 +212,7 @@ export function ConfirmEmailForm({
maxLength={6}
autoComplete="one-time-code"
autoFocus // eslint-disable-line jsx-a11y/no-autofocus
disabled={!!outerErrorDisplay}
/>
<div aria-live="polite">
{feedback?.type === 'input' && (
@@ -214,7 +227,7 @@ export function ConfirmEmailForm({
<div className="form-actions">
<OLButton
disabled={isResending}
disabled={isResending || !!outerErrorDisplay}
type="submit"
isLoading={isConfirming}
loadingLabel={t('confirming')}
@@ -3,6 +3,7 @@ import { UserEmailData } from '../../../../../../types/user-email'
import { ssoAvailableForInstitution } from '../../utils/sso'
import OLBadge from '@/features/ui/components/ol/ol-badge'
import ResendConfirmationCodeModal from '@/features/settings/components/emails/resend-confirmation-code-modal'
import { useUserEmailsContext } from '@/features/settings/context/user-email-context'
type EmailProps = {
userEmailData: UserEmailData
@@ -10,7 +11,11 @@ type EmailProps = {
function Email({ userEmailData }: EmailProps) {
const { t } = useTranslation()
const {
state,
setLoading: setUserEmailsContextLoading,
getEmails,
} = useUserEmailsContext()
const ssoAvailable = ssoAvailableForInstitution(
userEmailData.affiliation?.institution || null
)
@@ -30,7 +35,13 @@ function Email({ userEmailData }: EmailProps) {
<strong>{t('unconfirmed')}.</strong>
<br />
{!ssoAvailable && (
<ResendConfirmationCodeModal email={userEmailData.email} />
<ResendConfirmationCodeModal
email={userEmailData.email}
setGroupLoading={setUserEmailsContextLoading}
groupLoading={state.isLoading}
onSuccess={getEmails}
triggerVariant="link"
/>
)}
</div>
)}
@@ -1,10 +1,8 @@
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Icon from '../../../../shared/components/icon'
import { FetchError, postJSON } from '@/infrastructure/fetch-json'
import useAsync from '../../../../shared/hooks/use-async'
import { UserEmailData } from '../../../../../../types/user-email'
import { useUserEmailsContext } from '../../context/user-email-context'
import OLButton from '@/features/ui/components/ol/ol-button'
import OLModal, {
OLModalBody,
@@ -16,39 +14,33 @@ import { ConfirmEmailForm } from '@/features/settings/components/emails/confirm-
type ResendConfirmationEmailButtonProps = {
email: UserEmailData['email']
groupLoading: boolean
setGroupLoading: (loading: boolean) => void
onSuccess: () => void
triggerVariant: 'link' | 'secondary'
}
function ResendConfirmationCodeModal({
email,
groupLoading,
setGroupLoading,
onSuccess,
triggerVariant,
}: ResendConfirmationEmailButtonProps) {
const { t } = useTranslation()
const { error, isLoading, isError, runAsync } = useAsync()
const {
state,
setLoading: setUserEmailsContextLoading,
getEmails,
} = useUserEmailsContext()
const [modalVisible, setModalVisible] = useState(false)
// Update global isLoading prop
useEffect(() => {
setUserEmailsContextLoading(isLoading)
}, [setUserEmailsContextLoading, isLoading])
setGroupLoading(isLoading)
}, [isLoading, setGroupLoading])
const handleResendConfirmationEmail = async () => {
await runAsync(
postJSON('/user/emails/send-confirmation-code', { body: { email } })
)
.then(() => setModalVisible(true))
.catch(() => {})
}
if (isLoading) {
return (
<>
<Icon type="refresh" spin fw /> {t('sending')}&hellip;
</>
)
.finally(() => setModalVisible(true))
}
const rateLimited =
@@ -77,9 +69,16 @@ function ResendConfirmationCodeModal({
confirmationEndpoint="/user/emails/confirm-code"
email={email}
onSuccessfulConfirmation={() => {
getEmails()
onSuccess()
setModalVisible(false)
}}
outerError={
isError
? rateLimited
? t('too_many_requests')
: t('generic_something_went_wrong')
: undefined
}
/>
</OLModalBody>
<OLModalFooter>
@@ -94,21 +93,14 @@ function ResendConfirmationCodeModal({
</OLModal>
)}
<OLButton
variant="link"
disabled={state.isLoading || isLoading}
variant={triggerVariant}
disabled={groupLoading}
isLoading={isLoading}
onClick={handleResendConfirmationEmail}
className="btn-inline-link"
className={triggerVariant === 'link' ? 'btn-inline-link' : undefined}
>
{t('resend_confirmation_code')}
{t('send_confirmation_code')}
</OLButton>
<br />
{isError && (
<div className="text-danger">
{rateLimited
? t('too_many_requests')
: t('generic_something_went_wrong')}
</div>
)}
</>
)
}
@@ -10,6 +10,7 @@ export default function useFetchMock(
fetchMock.mockGlobal()
useLayoutEffect(() => {
fetchMock.mockGlobal()
callback(fetchMock)
return () => {
fetchMock.removeRoutes()
@@ -14,6 +14,8 @@ import {
setReconfirmationMeta,
} from './helpers/emails'
import { useMeta } from '../hooks/use-meta'
import { SplitTestProvider } from '@/shared/context/split-test-context'
import React, { ComponentType } from 'react'
export const ProjectInvite = (args: any) => {
useFetchMock(commonSetupMocks)
@@ -343,4 +345,11 @@ export const ReconfirmedAffiliationSuccess = (args: any) => {
export default {
title: 'Project List / Notifications',
component: UserNotifications,
decorators: [
(Story: ComponentType) => (
<SplitTestProvider>
<Story />
</SplitTestProvider>
),
],
}
@@ -0,0 +1,66 @@
import { SplitTestProvider } from '@/shared/context/split-test-context'
import UserNotifications from '../../../js/features/project-list/components/notifications/user-notifications'
import { ProjectListProvider } from '../../../js/features/project-list/context/project-list-context'
import { useMeta } from '../../hooks/use-meta'
import useFetchMock from '../../hooks/use-fetch-mock'
import ConfirmEmailNotification from '@/features/project-list/components/notifications/groups/confirm-email'
export const ConfirmEmail = (args: any) => {
useMeta({
'ol-userEmails': [
{
email: 'erika.mustermann+unconfirmed-primary@example.com',
default: true,
},
{ email: 'erika.mustermann+unconfirmed@example.com' },
{
email: 'erika.mustermann+untrusted@example.com',
lastConfirmedAt: '2019-01-01',
confirmedAt: '2019-01-01',
},
{
email: 'erika.mustermann+mit@example.com',
affiliation: {
institution: {
id: 123,
name: 'Massachusetts Institute of Technology',
confirmed: true,
commonsAccount: true,
},
},
},
],
'ol-user': { signUpDate: '2021-01-01' },
'ol-usersBestSubscription': { type: 'free' },
'ol-prefetchedProjectsBlob': { totalSize: 20 },
})
useFetchMock(fetchMock => {
fetchMock.post('/user/emails/send-confirmation-code', args.statusCode, {
delay: 500,
})
fetchMock.post('/user/emails/confirm-code', args.statusCode, {
delay: 500,
})
fetchMock.post('/user/emails/resend-confirmation-code', args.statusCode, {
delay: 500,
})
})
return (
<SplitTestProvider>
<ProjectListProvider>
<UserNotifications {...args} />
</ProjectListProvider>
</SplitTestProvider>
)
}
export default {
title: 'Project List / Notifications',
component: ConfirmEmailNotification,
args: {
statusCode: 200,
},
argTypes: {
statusCode: { type: 'select', options: [200, 400, 429] },
},
}
+3 -5
View File
@@ -669,7 +669,6 @@
"enabling": "Enabling",
"end_of_document": "End of document",
"ensure_recover_account": "This will ensure that it can be used to recover your __appName__ account in case you lose access to your primary email address.",
"enter_6_digit_code": "Enter 6-digit code",
"enter_any_size_including_units_or_valid_latex_command": "Enter any size (including units) or valid LaTeX command",
"enter_image_url": "Enter image URL",
"enter_the_code": "Enter the 6-digit code sent to __email__.",
@@ -1627,8 +1626,8 @@
"please_compile_pdf_before_download": "Please compile your project before downloading the PDF",
"please_compile_pdf_before_word_count": "Please compile your project before performing a word count",
"please_confirm_email": "Please confirm your email __emailAddress__ by clicking on the link in the confirmation email ",
"please_confirm_primary_email": "Please confirm your primary email address __emailAddress__ by clicking on the link in the confirmation email.",
"please_confirm_secondary_email": "Please confirm your secondary email address __emailAddress__ by clicking on the link in the confirmation email.",
"please_confirm_primary_email_or_edit": "Please confirm your primary email address __emailAddress__. To edit it, go to <0>Account settings</0>.",
"please_confirm_secondary_email_or_edit": "Please confirm your secondary email address __emailAddress__. To edit it, go to <0>Account settings</0>.",
"please_confirm_your_email_before_making_it_default": "Please confirm your email before making it the primary.",
"please_contact_support_to_makes_change_to_your_plan": "Please <0>contact Support</0> to make changes to your plan",
"please_contact_us_if_you_think_this_is_in_error": "Please <0>contact us</0> if you think this is in error.",
@@ -1810,7 +1809,6 @@
"remote_service_error": "The remote service produced an error",
"remove": "Remove",
"remove_access": "Remove access",
"remove_email_address": "Remove email address",
"remove_from_group": "Remove from group",
"remove_link": "Remove link",
"remove_manager": "Remove manager",
@@ -1853,7 +1851,6 @@
"resend_link_sso": "Resend SSO invite",
"resend_managed_user_invite": "Resend managed user invite",
"resending_confirmation_code": "Resending confirmation code",
"resending_confirmation_email": "Resending confirmation email",
"reset_password": "Reset Password",
"reset_password_link": "Click this link to reset your password",
"reset_password_sentence_case": "Reset password",
@@ -1987,6 +1984,7 @@
"selected_by_overleaf_staff": "Selected by Overleaf staff",
"selection_deleted": "Selection deleted",
"send": "Send",
"send_confirmation_code": "Send confirmation code",
"send_first_message": "Send your first message to your collaborators",
"send_message": "Send message",
"send_request": "Send request",
@@ -5,7 +5,6 @@ import {
render,
screen,
waitForElementToBeRemoved,
within,
} from '@testing-library/react'
import fetchMock from 'fetch-mock'
import { merge, cloneDeep } from 'lodash'
@@ -672,32 +671,37 @@ describe('<UserNotifications />', function () {
renderWithinProjectListProvider(ConfirmEmail)
await fetchMock.callHistory.flush(true)
fetchMock.post('/user/emails/resend_confirmation', 200)
fetchMock.post('/user/emails/send-confirmation-code', 200)
const email = userEmails[0].email
const notificationBody = await screen.findByTestId(
'pro-notification-body'
)
const alert = await screen.findByRole('alert')
if (isPrimary) {
expect(notificationBody.textContent).to.contain(
`Please confirm your primary email address ${email} by clicking on the link in the confirmation email.`
expect(alert.textContent).to.contain(
`Please confirm your primary email address ${email}. To edit it, go to `
)
} else {
expect(notificationBody.textContent).to.contain(
`Please confirm your secondary email address ${email} by clicking on the link in the confirmation email.`
expect(alert.textContent).to.contain(
`Please confirm your secondary email address ${email}. To edit it, go to `
)
}
const resendButton = screen.getByRole('button', { name: /resend/i })
fireEvent.click(resendButton)
expect(
screen
.getByRole('button', { name: 'Send confirmation code' })
.classList.contains('button-loading')
).to.be.false
await waitForElementToBeRemoved(() =>
screen.queryByRole('button', { name: /resend/i })
)
expect(screen.queryByRole('dialog')).to.be.null
const sendCodeButton = await screen.findByRole('button', {
name: 'Send confirmation code',
})
fireEvent.click(sendCodeButton)
await screen.findByRole('dialog')
expect(fetchMock.callHistory.called()).to.be.true
expect(screen.queryByRole('alert')).to.be.null
})
}
@@ -716,25 +720,22 @@ describe('<UserNotifications />', function () {
renderWithinProjectListProvider(ConfirmEmail)
await fetchMock.callHistory.flush(true)
fetchMock.post('/user/emails/resend_confirmation', 200)
fetchMock.post('/user/emails/send-confirmation-code', 200)
const email = untrustedUserData.email
const notificationBody = await screen.findByTestId(
'not-trusted-notification-body'
)
expect(notificationBody.textContent).to.contain(
const alert = await screen.findByRole('alert')
expect(alert.textContent).to.contain(
`To enhance the security of your Overleaf account, please reconfirm your secondary email address ${email}.`
)
const resendButton = screen.getByRole('button', { name: /resend/i })
const resendButton = screen.getByRole('button', {
name: 'Send confirmation code',
})
fireEvent.click(resendButton)
await waitForElementToBeRemoved(() =>
screen.getByRole('button', { name: /resend/i })
)
await screen.findByRole('dialog')
expect(fetchMock.callHistory.called()).to.be.true
expect(screen.queryByRole('alert')).to.be.null
})
it('fails to send', async function () {
@@ -742,20 +743,15 @@ describe('<UserNotifications />', function () {
renderWithinProjectListProvider(ConfirmEmail)
await fetchMock.callHistory.flush(true)
fetchMock.post('/user/emails/resend_confirmation', 500)
fetchMock.post('/user/emails/send-confirmation-code', 500)
const resendButtons = await screen.findAllByRole('button', {
name: /resend/i,
name: 'Send confirmation code',
})
const resendButton = resendButtons[0]
fireEvent.click(resendButton)
const notificationBody = screen.getByTestId('pro-notification-body')
await waitForElementToBeRemoved(() =>
within(notificationBody).getByTestId(
'loading-resending-confirmation-email'
)
)
await screen.findByRole('dialog')
expect(fetchMock.callHistory.called()).to.be.true
screen.getByText(/something went wrong/i)
@@ -773,11 +769,10 @@ describe('<UserNotifications />', function () {
const alert = await screen.findByRole('alert')
const email = unconfirmedCommonsUserData.email
const notificationBody = within(alert).getByTestId('notification-body')
expect(notificationBody.textContent).to.contain(
expect(alert.textContent).to.contain(
'You are one step away from accessing Overleaf Professional features'
)
expect(notificationBody.textContent).to.contain(
expect(alert.textContent).to.contain(
`Overleaf has an Overleaf subscription. Click the confirmation link sent to ${email} to upgrade to Overleaf Professional`
)
})
@@ -794,17 +789,14 @@ describe('<UserNotifications />', function () {
const alert = await screen.findByRole('alert')
const email = unconfirmedCommonsUserData.email
const notificationBody = within(alert).getByTestId(
'pro-notification-body'
)
const isPrimary = unconfirmedCommonsUserData.default
if (isPrimary) {
expect(notificationBody.textContent).to.contain(
`Please confirm your primary email address ${email} by clicking on the link in the confirmation email`
expect(alert.textContent).to.contain(
`Please confirm your primary email address ${email}.`
)
} else {
expect(notificationBody.textContent).to.contain(
`Please confirm your secondary email address ${email} by clicking on the link in the confirmation email`
expect(alert.textContent).to.contain(
`Please confirm your secondary email address ${email}.`
)
}
})
@@ -99,7 +99,7 @@ describe('<EmailsSection />', function () {
fetchMock.get('/user/emails?ensureAffiliation=true', [unconfirmedUserData])
render(<EmailsSection />)
await screen.findByRole('button', { name: /resend confirmation code/i })
await screen.findByRole('button', { name: 'Send confirmation code' })
})
it('renders professional label', async function () {
@@ -121,24 +121,24 @@ describe('<EmailsSection />', function () {
fetchMock.post('/user/emails/send-confirmation-code', 200)
const button = screen.getByRole('button', {
name: /resend confirmation code/i,
name: 'Send confirmation code',
})
fireEvent.click(button)
expect(
screen.queryByRole('button', {
name: /resend confirmation code/i,
name: 'Send confirmation code',
})
).to.be.null
await waitForElementToBeRemoved(() => screen.getByText(/sending/i))
await screen.findByRole('dialog')
expect(
screen.queryByText(/an error has occurred while performing your request/i)
).to.be.null
await screen.findAllByRole('button', {
name: /resend confirmation code/i,
name: 'Resend confirmation code',
})
})
@@ -151,17 +151,17 @@ describe('<EmailsSection />', function () {
fetchMock.post('/user/emails/send-confirmation-code', 503)
const button = screen.getByRole('button', {
name: /resend confirmation code/i,
name: 'Send confirmation code',
})
fireEvent.click(button)
expect(screen.queryByRole('button', { name: /resend confirmation code/i }))
.to.be.null
expect(screen.queryByRole('button', { name: 'Send confirmation code' })).to
.be.null
await waitForElementToBeRemoved(() => screen.getByText(/sending/i))
await screen.findByRole('dialog')
screen.getByText(/sorry, something went wrong/i)
screen.getByRole('button', { name: /resend confirmation code/i })
await screen.findByText(/sorry, something went wrong/i)
screen.getByRole('button', { name: 'Resend confirmation code' })
})
it('sorts emails with primary first, then confirmed, then unconfirmed', async function () {