diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 8381fd9fd4..cc02aea255 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -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": "", diff --git a/services/web/frontend/js/features/settings/components/emails/actions.tsx b/services/web/frontend/js/features/settings/components/emails/actions.tsx index d8ef82ca87..9ac715524d 100644 --- a/services/web/frontend/js/features/settings/components/emails/actions.tsx +++ b/services/web/frontend/js/features/settings/components/emails/actions.tsx @@ -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' diff --git a/services/web/frontend/js/features/settings/components/emails/actions/make-primary.tsx b/services/web/frontend/js/features/settings/components/emails/actions/make-primary.tsx deleted file mode 100644 index 092fe7ece1..0000000000 --- a/services/web/frontend/js/features/settings/components/emails/actions/make-primary.tsx +++ /dev/null @@ -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 ( - - {children} - - ) -} - -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 {t('sending')}... - } - - return ( - - {/* - Disabled buttons don't work with tooltips, due to pointer-events: none, - so create a wrapper for the tooltip - */} - - - {t('make_primary')} - - - - ) -} - -export default MakePrimary diff --git a/services/web/frontend/js/features/settings/components/emails/actions/make-primary/confirmation-modal.tsx b/services/web/frontend/js/features/settings/components/emails/actions/make-primary/confirmation-modal.tsx new file mode 100644 index 0000000000..8009ebb9b9 --- /dev/null +++ b/services/web/frontend/js/features/settings/components/emails/actions/make-primary/confirmation-modal.tsx @@ -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, + { + email: string + isConfirmDisabled: boolean + onConfirm: () => void + onHide: () => void + } +> + +function ConfirmationModal({ + email, + isConfirmDisabled, + show, + onConfirm, + onHide, +}: ConfirmationModalProps) { + const { t } = useTranslation() + + return ( + + + {t('confirm_primary_email_change')} + + + + }} + values={{ email }} + /> + + {t('log_in_with_primary_email_address')} + + + + {t('cancel')} + + + {t('confirm')} + + + + ) +} + +export default ConfirmationModal diff --git a/services/web/frontend/js/features/settings/components/emails/actions/make-primary/make-primary.tsx b/services/web/frontend/js/features/settings/components/emails/actions/make-primary/make-primary.tsx new file mode 100644 index 0000000000..8e7f8bd391 --- /dev/null +++ b/services/web/frontend/js/features/settings/components/emails/actions/make-primary/make-primary.tsx @@ -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 ? ( + {t('sending')}... + ) : ( + + {/* + Disabled buttons don't work with tooltips, due to pointer-events: none, + so create a wrapper for the tooltip + */} + + + {t('make_primary')} + + + + )} + + > + ) +} + +export default MakePrimary diff --git a/services/web/frontend/js/features/settings/components/emails/actions/make-primary/primary-button.tsx b/services/web/frontend/js/features/settings/components/emails/actions/make-primary/primary-button.tsx new file mode 100644 index 0000000000..089856d7f2 --- /dev/null +++ b/services/web/frontend/js/features/settings/components/emails/actions/make-primary/primary-button.tsx @@ -0,0 +1,17 @@ +import { Button } from 'react-bootstrap' + +function PrimaryButton({ children, disabled, onClick }: Button.ButtonProps) { + return ( + + {children} + + ) +} + +export default PrimaryButton diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 11dd7cdd6a..8b9519523f 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -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 __appName__ account, or if you don’t want to link to your __institutionName__ account, please click __clickText__.", "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 __email__?", "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 __appName__ account in order to get your __appName__ and __institutionName__ 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.", diff --git a/services/web/test/frontend/features/settings/components/emails/emails-section-actions.test.tsx b/services/web/test/frontend/features/settings/components/emails/emails-section-actions.test.tsx index fdc7f12b3b..479a97f035 100644 --- a/services/web/test/frontend/features/settings/components/emails/emails-section-actions.test.tsx +++ b/services/web/test/frontend/features/settings/components/emails/emails-section-actions.test.tsx @@ -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 () { - fetchMock - .get('/user/emails?ensureAffiliation=true', [userEmailData]) - .post('/user/emails/default', 200) - const userEmailDataCopy = { ...userEmailData } - render() + describe('make an email primary', function () { + const confirmPrimaryEmail = async () => { + const button = await screen.findByRole('button', { + name: /make primary/i, + }) + fireEvent.click(button) - 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 - expect(screen.queryByRole('button', { name: /make primary/i })).to.be.null + await waitForElementToBeRemoved(() => + screen.getByRole('button', { name: /sending/i, hidden: true }) + ) + } - userEmailDataCopy.default = true + it('shows confirmation modal and closes it', async function () { + fetchMock.get('/user/emails?ensureAffiliation=true', [userEmailData]) + render() - await waitForElementToBeRemoved(() => - screen.getByRole('button', { name: /sending/i }) - ) + const button = await screen.findByRole('button', { + name: /make primary/i, + }) + fireEvent.click(button) - expect( - 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 - }) + 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 }) - it('shows error when making email primary', async function () { - fetchMock - .get('/user/emails?ensureAffiliation=true', [userEmailData]) - .post('/user/emails/default', 503) - render() + fireEvent.click(withinModal.getByRole('button', { name: /cancel/i })) + expect(screen.queryByRole('dialog')).to.be.null + }) - const button = await screen.findByRole('button', { name: /make primary/i }) - fireEvent.click(button) + it('shows loader and removes button', async function () { + fetchMock + .get('/user/emails?ensureAffiliation=true', [userEmailData]) + .post('/user/emails/default', 200) + render() - await waitForElementToBeRemoved(() => - screen.getByRole('button', { name: /sending/i }) - ) + await confirmPrimaryEmail() - screen.getByText(/sorry, something went wrong/i) - screen.getByRole('button', { name: /make primary/i }) + expect( + 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', async function () { + fetchMock + .get('/user/emails?ensureAffiliation=true', [userEmailData]) + .post('/user/emails/default', 503) + render() + + await confirmPrimaryEmail() + + screen.getByText(/sorry, something went wrong/i) + await screen.findByRole('button', { name: /make primary/i }) + }) }) })
+ }} + values={{ email }} + /> +
{t('log_in_with_primary_email_address')}