Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: change email updating workflow [DHIS2-18493] #1470

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 55 additions & 10 deletions i18n/en.pot
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ msgstr ""
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"POT-Creation-Date: 2025-01-07T11:04:56.155Z\n"
"PO-Revision-Date: 2025-01-07T11:04:56.155Z\n"
"POT-Creation-Date: 2025-01-13T17:35:56.021Z\n"
"PO-Revision-Date: 2025-01-13T17:35:56.021Z\n"

msgid "Never"
msgstr "Never"
Expand Down Expand Up @@ -307,6 +307,54 @@ msgstr "Select profile picture"
msgid "Remove profile picture"
msgstr "Remove profile picture"

msgid "No email provided"
msgstr "No email provided"

msgid "Email is invalid"
msgstr "Email is invalid"

msgid "Emails must match"
msgstr "Emails must match"

msgid "Remove email"
msgstr "Remove email"

msgid "Your email is currently verified"
msgstr "Your email is currently verified"

msgid "Are you sure you want to remove your email?"
msgstr "Are you sure you want to remove your email?"

msgid "Cancel"
msgstr "Cancel"

msgid "Change email"
msgstr "Change email"

msgid "If you change your email, you may need to reverify your email."
msgstr "If you change your email, you may need to reverify your email."

msgid "Current email"
msgstr "Current email"

msgid "no current email"
msgstr "no current email"

msgid "Enter new email"
msgstr "Enter new email"

msgid "Confirm new email"
msgstr "Confirm new email"

msgid "Save"
msgstr "Save"

msgid "Email"
msgstr "Email"

msgid "There is no email to remove"
msgstr "There is no email to remove"

msgid "This field is required"
msgstr "This field is required"

Expand Down Expand Up @@ -373,8 +421,8 @@ msgstr "Email verification link sent successfully!"
msgid "Failed to send email verification link."
msgstr "Failed to send email verification link."

msgid "Verify Email"
msgstr "Verify Email"
msgid "Verify email"
msgstr "Verify email"

msgid ""
"Your email is not verified. Please verify your email to continue using the "
Expand All @@ -383,6 +431,9 @@ msgstr ""
"Your email is not verified. Please verify your email to continue using the "
"system."

msgid "Please provide an email and verify it to continue using the system."
msgstr "Please provide an email and verify it to continue using the system."

msgid "Manage personal access tokens"
msgstr "Manage personal access tokens"

Expand Down Expand Up @@ -522,9 +573,6 @@ msgstr ""
msgid "Token details"
msgstr "Token details"

msgid "Cancel"
msgstr "Cancel"

msgid ""
"Important: IP address validation relies on the X-Forwarded-For header, "
"which can be spoofed. For security, make sure a load balancer or reverse "
Expand Down Expand Up @@ -607,9 +655,6 @@ msgstr "Other"
msgid "E-mail"
msgstr "E-mail"

msgid "E-mail Verification"
msgstr "E-mail Verification"

msgid "Mobile phone number"
msgstr "Mobile phone number"

Expand Down
261 changes: 261 additions & 0 deletions src/layout/EmailField.component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
import i18n from '@dhis2/d2-i18n'
import {
Button,
ButtonStrip,
email as emailValidator,
InputField,
Modal,
ModalActions,
ModalContent,
ModalTitle,
NoticeBox,
Tooltip,
} from '@dhis2/ui'
import TextField from 'd2-ui/lib/form-fields/TextField'
import PropTypes from 'prop-types'
import React, { useMemo, useState } from 'react'
import styles from './EmailField.component.module.css'
import { VerifyEmail } from './VerifyEmail.component.js'

const TooltipWrapper = ({ disabled, content, children }) => {
if (!disabled) {
return <>{children}</>
}
return <Tooltip content={content}>{children}</Tooltip>
}

TooltipWrapper.propTypes = {
children: PropTypes.node,
content: PropTypes.string,
disabled: PropTypes.bool,
}

const getSaveDisabledContent = ({ newEmail, emailValidationMessage }) => {
if (!newEmail) {
return i18n.t('No email provided')
}
if (emailValidationMessage) {
return i18n.t('Email is invalid')
}
return i18n.t('Emails must match')
}

const RemoveModal = ({
removeModalOpen,
setRemoveModalOpen,
userEmailVerified,
onUpdate,
}) => (
<Modal hide={!removeModalOpen} onClose={() => setRemoveModalOpen(false)}>
<ModalTitle>{i18n.t('Remove email')}</ModalTitle>

<ModalContent>
{userEmailVerified && (
<NoticeBox
className={styles.emailModalItem}
title={i18n.t('Your email is currently verified')}
warning
></NoticeBox>
)}
<div>{i18n.t('Are you sure you want to remove your email?')}</div>
</ModalContent>

<ModalActions>
<ButtonStrip end>
<Button onClick={() => setRemoveModalOpen(false)} secondary>
{i18n.t('Cancel')}
</Button>

<Button
onClick={() => {
onUpdate('')
setRemoveModalOpen(false)
}}
destructive
>
{i18n.t('Remove email')}
</Button>
</ButtonStrip>
</ModalActions>
</Modal>
)

RemoveModal.propTypes = {
removeModalOpen: PropTypes.bool,
setRemoveModalOpen: PropTypes.func,
userEmailVerified: PropTypes.bool,
onUpdate: PropTypes.func,
}

const EmailModal = ({
emailModalOpen,
setEmailModalOpen,
userEmailVerified,
userEmail,
onUpdate,
}) => {
const [newEmail, setNewEmail] = useState()
const [newEmailConfirm, setNewEmailConfirm] = useState()
const [newEmailConfirmTouched, setNewEmailConfirmTouched] = useState(false)
const emailValidationMessage = useMemo(
() => emailValidator(newEmail),
[newEmail]
)
const emailsMatch = newEmail === newEmailConfirm
const saveDisabled =
!newEmail || Boolean(emailValidationMessage) || !emailsMatch
const saveDisabledContent = getSaveDisabledContent({
newEmail,
emailValidationMessage,
})

return (
<Modal
hide={!emailModalOpen}
onClose={() => {
setEmailModalOpen(false)
}}
>
<ModalTitle>{i18n.t('Change email')}</ModalTitle>

<ModalContent>
{userEmailVerified && (
<NoticeBox
className={styles.emailModalItem}
title={i18n.t('Your email is currently verified')}
warning
>
{i18n.t(
'If you change your email, you may need to reverify your email.'
)}
</NoticeBox>
)}

<InputField
label={i18n.t('Current email')}
value={
userEmail?.trim() !== ''
? userEmail
: i18n.t('no current email')
}
type="email"
disabled
className={styles.emailModalItem}
/>
<InputField
label={i18n.t('Enter new email')}
value={newEmail}
type="email"
error={Boolean(emailValidationMessage)}
validationText={emailValidationMessage}
onChange={(newValue) => setNewEmail(newValue.value)}
className={styles.emailModalItem}
/>

<InputField
label={i18n.t('Confirm new email')}
value={newEmailConfirm}
type="email"
error={newEmailConfirmTouched && !emailsMatch}
validationText={
emailsMatch || !newEmailConfirmTouched
? undefined
: i18n.t('Emails must match')
}
onChange={(newValue) => {
setNewEmailConfirmTouched(true)
setNewEmailConfirm(newValue.value)
}}
className={styles.emailModalItem}
/>
</ModalContent>

<ModalActions>
<ButtonStrip end>
<Button onClick={() => setEmailModalOpen(false)} secondary>
{i18n.t('Cancel')}
</Button>

<TooltipWrapper
disabled={saveDisabled}
content={saveDisabledContent}
>
<Button
onClick={() => {
onUpdate(newEmail)
setEmailModalOpen(false)
}}
primary
disabled={saveDisabled}
>
{i18n.t('Save')}
</Button>
</TooltipWrapper>
</ButtonStrip>
</ModalActions>
</Modal>
)
}

EmailModal.propTypes = {
emailModalOpen: PropTypes.bool,
setEmailModalOpen: PropTypes.func,
userEmail: PropTypes.string,
userEmailVerified: PropTypes.bool,
onUpdate: PropTypes.func,
}

export function EmailField({ userEmail, userEmailVerified, onUpdate }) {
const [emailModalOpen, setEmailModalOpen] = useState()
const [removeModalOpen, setRemoveModalOpen] = useState()

return (
<div className={styles.emailModalContainer}>
<TextField
value={userEmail}
disabled
floatingLabelText={i18n.t('Email')}
style={{ width: '100%' }}
/>
<div className={styles.buttonContainer}>
<VerifyEmail userEmail={userEmail} />
<Button secondary onClick={() => setEmailModalOpen(true)}>
{i18n.t('Change email')}
</Button>
<TooltipWrapper
disabled={!userEmail || userEmail?.trim() === ''}
content={i18n.t('There is no email to remove')}
>
<Button
destructive
onClick={() => setRemoveModalOpen(true)}
disabled={!userEmail || userEmail?.trim() === ''}
>
{i18n.t('Remove email')}
</Button>
</TooltipWrapper>
</div>
{emailModalOpen && (
<EmailModal
emailModalOpen={emailModalOpen}
setEmailModalOpen={setEmailModalOpen}
userEmailVerified={userEmailVerified}
userEmail={userEmail}
onUpdate={onUpdate}
/>
)}
<RemoveModal
removeModalOpen={removeModalOpen}
setRemoveModalOpen={setRemoveModalOpen}
userEmailVerified={userEmailVerified}
onUpdate={onUpdate}
/>
</div>
)
}

EmailField.propTypes = {
userEmail: PropTypes.string,
userEmailVerified: PropTypes.bool,
onUpdate: PropTypes.func,
}
16 changes: 16 additions & 0 deletions src/layout/EmailField.component.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.emailModalContainer {
margin-block-end: 8px;
}

.emailTextField {
width: 100%;
}

.emailModalItem {
margin-block-end: 16px;
}

.buttonContainer {
display: flex;
gap: 8px;
}
Loading
Loading