mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #12278 from overleaf/ii-change-email-confirmation
[web] Show confirmation modal when making an email primary GitOrigin-RevId: eb67fc37c18a5ecd59973baa27ee9ef8e4b67423
This commit is contained in:
parent
3a071c247a
commit
122d2310e6
8 changed files with 262 additions and 143 deletions
|
@ -134,6 +134,7 @@
|
|||
"confirm_affiliation": "",
|
||||
"confirm_affiliation_to_relink_dropbox": "",
|
||||
"confirm_new_password": "",
|
||||
"confirm_primary_email_change": "",
|
||||
"conflicting_paths_found": "",
|
||||
"connected_users": "",
|
||||
"contact_message_label": "",
|
||||
|
@ -183,6 +184,7 @@
|
|||
"discount_of": "",
|
||||
"dismiss": "",
|
||||
"dismiss_error_popup": "",
|
||||
"do_you_want_to_change_your_primary_email_address_to": "",
|
||||
"do_you_want_to_overwrite_them": "",
|
||||
"documentation": "",
|
||||
"doesnt_match": "",
|
||||
|
@ -451,6 +453,7 @@
|
|||
"log_entry_maximum_entries_see_full_logs": "",
|
||||
"log_entry_maximum_entries_title": "",
|
||||
"log_hint_extra_info": "",
|
||||
"log_in_with_primary_email_address": "",
|
||||
"log_viewer_error": "",
|
||||
"login_with_service": "",
|
||||
"logs_and_output_files": "",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import MakePrimary from './actions/make-primary'
|
||||
import MakePrimary from './actions/make-primary/make-primary'
|
||||
import Remove from './actions/remove'
|
||||
import useAsync from '../../../../shared/hooks/use-async'
|
||||
import { useUserEmailsContext } from '../../context/user-email-context'
|
||||
|
|
|
@ -1,112 +0,0 @@
|
|||
import Tooltip from '../../../../../shared/components/tooltip'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
inReconfirmNotificationPeriod,
|
||||
institutionAlreadyLinked,
|
||||
} from '../../../utils/selectors'
|
||||
import { postJSON } from '../../../../../infrastructure/fetch-json'
|
||||
import {
|
||||
State,
|
||||
useUserEmailsContext,
|
||||
} from '../../../context/user-email-context'
|
||||
import { UserEmailData } from '../../../../../../../types/user-email'
|
||||
import { UseAsyncReturnType } from '../../../../../shared/hooks/use-async'
|
||||
import { ssoAvailableForInstitution } from '../../../utils/sso'
|
||||
|
||||
const getDescription = (
|
||||
t: (s: string) => string,
|
||||
state: State,
|
||||
userEmailData: UserEmailData
|
||||
) => {
|
||||
if (inReconfirmNotificationPeriod(userEmailData)) {
|
||||
return t('please_reconfirm_your_affiliation_before_making_this_primary')
|
||||
}
|
||||
|
||||
if (userEmailData.confirmedAt) {
|
||||
return t('make_email_primary_description')
|
||||
}
|
||||
|
||||
const ssoAvailable = ssoAvailableForInstitution(
|
||||
userEmailData.affiliation?.institution || null
|
||||
)
|
||||
|
||||
if (!institutionAlreadyLinked(state, userEmailData) && ssoAvailable) {
|
||||
return t('please_link_before_making_primary')
|
||||
}
|
||||
|
||||
return t('please_confirm_your_email_before_making_it_default')
|
||||
}
|
||||
|
||||
function PrimaryButton({ children, disabled, onClick }: Button.ButtonProps) {
|
||||
return (
|
||||
<Button
|
||||
bsSize="small"
|
||||
bsStyle={null}
|
||||
className="btn-secondary-info btn-secondary"
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
type MakePrimaryProps = {
|
||||
userEmailData: UserEmailData
|
||||
makePrimaryAsync: UseAsyncReturnType
|
||||
}
|
||||
|
||||
function MakePrimary({ userEmailData, makePrimaryAsync }: MakePrimaryProps) {
|
||||
const { t } = useTranslation()
|
||||
const { state, makePrimary } = useUserEmailsContext()
|
||||
|
||||
const handleSetDefaultUserEmail = () => {
|
||||
makePrimaryAsync
|
||||
.runAsync(
|
||||
postJSON('/user/emails/default', {
|
||||
body: {
|
||||
email: userEmailData.email,
|
||||
},
|
||||
})
|
||||
)
|
||||
.then(() => {
|
||||
makePrimary(userEmailData.email)
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
if (userEmailData.default) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (makePrimaryAsync.isLoading) {
|
||||
return <PrimaryButton disabled>{t('sending')}...</PrimaryButton>
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
id={`make-primary-${userEmailData.email}`}
|
||||
description={getDescription(t, state, userEmailData)}
|
||||
>
|
||||
{/*
|
||||
Disabled buttons don't work with tooltips, due to pointer-events: none,
|
||||
so create a wrapper for the tooltip
|
||||
*/}
|
||||
<span>
|
||||
<PrimaryButton
|
||||
disabled={
|
||||
!userEmailData.confirmedAt ||
|
||||
state.isLoading ||
|
||||
inReconfirmNotificationPeriod(userEmailData)
|
||||
}
|
||||
onClick={handleSetDefaultUserEmail}
|
||||
>
|
||||
{t('make_primary')}
|
||||
</PrimaryButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export default MakePrimary
|
|
@ -0,0 +1,62 @@
|
|||
import { useTranslation, Trans } from 'react-i18next'
|
||||
import { Modal, Button } from 'react-bootstrap'
|
||||
import AccessibleModal from '../../../../../../shared/components/accessible-modal'
|
||||
import { MergeAndOverride } from '../../../../../../../../types/utils'
|
||||
|
||||
type ConfirmationModalProps = MergeAndOverride<
|
||||
React.ComponentProps<typeof AccessibleModal>,
|
||||
{
|
||||
email: string
|
||||
isConfirmDisabled: boolean
|
||||
onConfirm: () => void
|
||||
onHide: () => void
|
||||
}
|
||||
>
|
||||
|
||||
function ConfirmationModal({
|
||||
email,
|
||||
isConfirmDisabled,
|
||||
show,
|
||||
onConfirm,
|
||||
onHide,
|
||||
}: ConfirmationModalProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<AccessibleModal show={show} onHide={onHide}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>{t('confirm_primary_email_change')}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body className="modal-body-share">
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey="do_you_want_to_change_your_primary_email_address_to"
|
||||
components={{ b: <b /> }}
|
||||
values={{ email }}
|
||||
/>
|
||||
</p>
|
||||
<p className="mb-0">{t('log_in_with_primary_email_address')}</p>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
bsStyle={null}
|
||||
className="btn-secondary-info btn-secondary"
|
||||
onClick={onHide}
|
||||
>
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
bsStyle={null}
|
||||
className="btn-primary"
|
||||
disabled={isConfirmDisabled}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
{t('confirm')}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</AccessibleModal>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConfirmationModal
|
|
@ -0,0 +1,116 @@
|
|||
import { useState } from 'react'
|
||||
import Tooltip from '../../../../../../shared/components/tooltip'
|
||||
import PrimaryButton from './primary-button'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
inReconfirmNotificationPeriod,
|
||||
institutionAlreadyLinked,
|
||||
} from '../../../../utils/selectors'
|
||||
import { postJSON } from '../../../../../../infrastructure/fetch-json'
|
||||
import {
|
||||
State,
|
||||
useUserEmailsContext,
|
||||
} from '../../../../context/user-email-context'
|
||||
import { UserEmailData } from '../../../../../../../../types/user-email'
|
||||
import { UseAsyncReturnType } from '../../../../../../shared/hooks/use-async'
|
||||
import { ssoAvailableForInstitution } from '../../../../utils/sso'
|
||||
import ConfirmationModal from './confirmation-modal'
|
||||
|
||||
const getDescription = (
|
||||
t: (s: string) => string,
|
||||
state: State,
|
||||
userEmailData: UserEmailData
|
||||
) => {
|
||||
if (inReconfirmNotificationPeriod(userEmailData)) {
|
||||
return t('please_reconfirm_your_affiliation_before_making_this_primary')
|
||||
}
|
||||
|
||||
if (userEmailData.confirmedAt) {
|
||||
return t('make_email_primary_description')
|
||||
}
|
||||
|
||||
const ssoAvailable = ssoAvailableForInstitution(
|
||||
userEmailData.affiliation?.institution || null
|
||||
)
|
||||
|
||||
if (!institutionAlreadyLinked(state, userEmailData) && ssoAvailable) {
|
||||
return t('please_link_before_making_primary')
|
||||
}
|
||||
|
||||
return t('please_confirm_your_email_before_making_it_default')
|
||||
}
|
||||
|
||||
type MakePrimaryProps = {
|
||||
userEmailData: UserEmailData
|
||||
makePrimaryAsync: UseAsyncReturnType
|
||||
}
|
||||
|
||||
function MakePrimary({ userEmailData, makePrimaryAsync }: MakePrimaryProps) {
|
||||
const [show, setShow] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
const { state, makePrimary } = useUserEmailsContext()
|
||||
|
||||
const handleShowModal = () => setShow(true)
|
||||
const handleHideModal = () => setShow(false)
|
||||
const handleSetDefaultUserEmail = () => {
|
||||
handleHideModal()
|
||||
|
||||
makePrimaryAsync
|
||||
.runAsync(
|
||||
postJSON('/user/emails/default', {
|
||||
body: {
|
||||
email: userEmailData.email,
|
||||
},
|
||||
})
|
||||
)
|
||||
.then(() => {
|
||||
makePrimary(userEmailData.email)
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
if (userEmailData.default) {
|
||||
return null
|
||||
}
|
||||
|
||||
const isConfirmDisabled = Boolean(
|
||||
!userEmailData.confirmedAt ||
|
||||
state.isLoading ||
|
||||
inReconfirmNotificationPeriod(userEmailData)
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
{makePrimaryAsync.isLoading ? (
|
||||
<PrimaryButton disabled>{t('sending')}...</PrimaryButton>
|
||||
) : (
|
||||
<Tooltip
|
||||
id={`make-primary-${userEmailData.email}`}
|
||||
description={getDescription(t, state, userEmailData)}
|
||||
>
|
||||
{/*
|
||||
Disabled buttons don't work with tooltips, due to pointer-events: none,
|
||||
so create a wrapper for the tooltip
|
||||
*/}
|
||||
<span>
|
||||
<PrimaryButton
|
||||
disabled={isConfirmDisabled}
|
||||
onClick={handleShowModal}
|
||||
>
|
||||
{t('make_primary')}
|
||||
</PrimaryButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
<ConfirmationModal
|
||||
email={userEmailData.email}
|
||||
isConfirmDisabled={isConfirmDisabled}
|
||||
show={show}
|
||||
onHide={handleHideModal}
|
||||
onConfirm={handleSetDefaultUserEmail}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default MakePrimary
|
|
@ -0,0 +1,17 @@
|
|||
import { Button } from 'react-bootstrap'
|
||||
|
||||
function PrimaryButton({ children, disabled, onClick }: Button.ButtonProps) {
|
||||
return (
|
||||
<Button
|
||||
bsSize="small"
|
||||
bsStyle={null}
|
||||
className="btn-secondary-info btn-secondary"
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export default PrimaryButton
|
|
@ -255,6 +255,7 @@
|
|||
"confirm_affiliation_to_relink_dropbox": "Please confirm you are still at the institution and on their license, or upgrade your account in order to relink your Dropbox account.",
|
||||
"confirm_email": "Confirm Email",
|
||||
"confirm_new_password": "Confirm New Password",
|
||||
"confirm_primary_email_change": "Confirm primary email change",
|
||||
"confirmation_link_broken": "Sorry, something is wrong with your confirmation link. Please try copy and pasting the link from the bottom of your confirmation email.",
|
||||
"confirmation_token_invalid": "Sorry, your confirmation token is invalid or has expired. Please request a new email confirmation link.",
|
||||
"confirming": "Confirming",
|
||||
|
@ -342,6 +343,7 @@
|
|||
"dismiss_error_popup": "Dismiss first error alert",
|
||||
"do_not_have_acct_or_do_not_want_to_link": "If you don’t have an <b>__appName__</b> account, or if you don’t want to link to your <b>__institutionName__</b> account, please click <b>__clickText__</b>.",
|
||||
"do_not_link_accounts": "Don’t link accounts",
|
||||
"do_you_want_to_change_your_primary_email_address_to": "Do you want to change your primary email address to <b>__email__</b>?",
|
||||
"do_you_want_to_overwrite_them": "Do you want to overwrite them?",
|
||||
"document_history": "Document history",
|
||||
"documentation": "Documentation",
|
||||
|
@ -854,6 +856,7 @@
|
|||
"log_in_with": "Log in with __provider__",
|
||||
"log_in_with_email": "Log in with __email__",
|
||||
"log_in_with_existing_institution_email": "Please log in with your existing <b>__appName__</b> account in order to get your <b>__appName__</b> and <b>__institutionName__</b> institutional accounts linked.",
|
||||
"log_in_with_primary_email_address": "This will be the email address to use if you log in with an email address and password. Important __appName__ notifications will be sent to this email address.",
|
||||
"log_out": "Log Out",
|
||||
"log_out_from": "Log out from __email__",
|
||||
"log_viewer_error": "There was a problem displaying this project’s compilation errors and logs.",
|
||||
|
|
|
@ -2,6 +2,7 @@ import {
|
|||
render,
|
||||
screen,
|
||||
waitForElementToBeRemoved,
|
||||
within,
|
||||
fireEvent,
|
||||
} from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
@ -160,45 +161,74 @@ describe('email actions - make primary', function () {
|
|||
})
|
||||
})
|
||||
|
||||
it('shows loader when making email primary and removes button', async function () {
|
||||
describe('make an email primary', function () {
|
||||
const confirmPrimaryEmail = async () => {
|
||||
const button = await screen.findByRole('button', {
|
||||
name: /make primary/i,
|
||||
})
|
||||
fireEvent.click(button)
|
||||
|
||||
const withinModal = within(screen.getByRole('dialog'))
|
||||
fireEvent.click(withinModal.getByRole('button', { name: /confirm/i }))
|
||||
expect(screen.queryByRole('dialog')).to.be.null
|
||||
|
||||
await waitForElementToBeRemoved(() =>
|
||||
screen.getByRole('button', { name: /sending/i, hidden: true })
|
||||
)
|
||||
}
|
||||
|
||||
it('shows confirmation modal and closes it', async function () {
|
||||
fetchMock.get('/user/emails?ensureAffiliation=true', [userEmailData])
|
||||
render(<EmailsSection />)
|
||||
|
||||
const button = await screen.findByRole('button', {
|
||||
name: /make primary/i,
|
||||
})
|
||||
fireEvent.click(button)
|
||||
|
||||
const withinModal = within(screen.getByRole('dialog'))
|
||||
withinModal.getByText(
|
||||
/do you want to change your primary email address to .*/i
|
||||
)
|
||||
withinModal.getByText(
|
||||
/this will be the email address to use if you log in with an email address and password/i
|
||||
)
|
||||
withinModal.getByText(
|
||||
/important .* notifications will be sent to this email address/i
|
||||
)
|
||||
withinModal.getByRole('button', { name: /confirm/i })
|
||||
|
||||
fireEvent.click(withinModal.getByRole('button', { name: /cancel/i }))
|
||||
expect(screen.queryByRole('dialog')).to.be.null
|
||||
})
|
||||
|
||||
it('shows loader and removes button', async function () {
|
||||
fetchMock
|
||||
.get('/user/emails?ensureAffiliation=true', [userEmailData])
|
||||
.post('/user/emails/default', 200)
|
||||
const userEmailDataCopy = { ...userEmailData }
|
||||
render(<EmailsSection />)
|
||||
|
||||
const button = await screen.findByRole('button', { name: /make primary/i })
|
||||
fireEvent.click(button)
|
||||
|
||||
expect(screen.queryByRole('button', { name: /make primary/i })).to.be.null
|
||||
|
||||
userEmailDataCopy.default = true
|
||||
|
||||
await waitForElementToBeRemoved(() =>
|
||||
screen.getByRole('button', { name: /sending/i })
|
||||
)
|
||||
await confirmPrimaryEmail()
|
||||
|
||||
expect(
|
||||
screen.queryByText(/an error has occurred while performing your request/i)
|
||||
screen.queryByText(
|
||||
/an error has occurred while performing your request/i
|
||||
)
|
||||
).to.be.null
|
||||
expect(screen.queryByRole('button', { name: /make primary/i })).to.be.null
|
||||
})
|
||||
|
||||
it('shows error when making email primary', async function () {
|
||||
it('shows error', async function () {
|
||||
fetchMock
|
||||
.get('/user/emails?ensureAffiliation=true', [userEmailData])
|
||||
.post('/user/emails/default', 503)
|
||||
render(<EmailsSection />)
|
||||
|
||||
const button = await screen.findByRole('button', { name: /make primary/i })
|
||||
fireEvent.click(button)
|
||||
|
||||
await waitForElementToBeRemoved(() =>
|
||||
screen.getByRole('button', { name: /sending/i })
|
||||
)
|
||||
await confirmPrimaryEmail()
|
||||
|
||||
screen.getByText(/sorry, something went wrong/i)
|
||||
screen.getByRole('button', { name: /make primary/i })
|
||||
await screen.findByRole('button', { name: /make primary/i })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
|
Loading…
Reference in a new issue