mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-06-05 23:29:00 +02:00
[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:
@@ -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": "",
|
||||
|
||||
+94
-157
@@ -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
|
||||
})}
|
||||
|
||||
+22
-9
@@ -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>
|
||||
)}
|
||||
|
||||
+24
-32
@@ -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')}…
|
||||
</>
|
||||
)
|
||||
.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] },
|
||||
},
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
+35
-43
@@ -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}.`
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
+11
-11
@@ -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 () {
|
||||
|
||||
Reference in New Issue
Block a user