diff --git a/i18n/en.pot b/i18n/en.pot
index fd723b7b..598cf9dc 100644
--- a/i18n/en.pot
+++ b/i18n/en.pot
@@ -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"
@@ -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"
@@ -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 "
@@ -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"
@@ -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 "
@@ -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"
diff --git a/src/layout/EmailField.component.js b/src/layout/EmailField.component.js
new file mode 100644
index 00000000..c44a1926
--- /dev/null
+++ b/src/layout/EmailField.component.js
@@ -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 {children}
+}
+
+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,
+}) => (
+ setRemoveModalOpen(false)}>
+ {i18n.t('Remove email')}
+
+
+ {userEmailVerified && (
+
+ )}
+ {i18n.t('Are you sure you want to remove your email?')}
+
+
+
+
+ setRemoveModalOpen(false)} secondary>
+ {i18n.t('Cancel')}
+
+
+ {
+ onUpdate('')
+ setRemoveModalOpen(false)
+ }}
+ destructive
+ >
+ {i18n.t('Remove email')}
+
+
+
+
+)
+
+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 (
+ {
+ setEmailModalOpen(false)
+ }}
+ >
+ {i18n.t('Change email')}
+
+
+ {userEmailVerified && (
+
+ {i18n.t(
+ 'If you change your email, you may need to reverify your email.'
+ )}
+
+ )}
+
+
+ setNewEmail(newValue.value)}
+ className={styles.emailModalItem}
+ />
+
+ {
+ setNewEmailConfirmTouched(true)
+ setNewEmailConfirm(newValue.value)
+ }}
+ className={styles.emailModalItem}
+ />
+
+
+
+
+ setEmailModalOpen(false)} secondary>
+ {i18n.t('Cancel')}
+
+
+
+ {
+ onUpdate(newEmail)
+ setEmailModalOpen(false)
+ }}
+ primary
+ disabled={saveDisabled}
+ >
+ {i18n.t('Save')}
+
+
+
+
+
+ )
+}
+
+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 (
+
+
+
+
+ setEmailModalOpen(true)}>
+ {i18n.t('Change email')}
+
+
+ setRemoveModalOpen(true)}
+ disabled={!userEmail || userEmail?.trim() === ''}
+ >
+ {i18n.t('Remove email')}
+
+
+
+ {emailModalOpen && (
+
+ )}
+
+
+ )
+}
+
+EmailField.propTypes = {
+ userEmail: PropTypes.string,
+ userEmailVerified: PropTypes.bool,
+ onUpdate: PropTypes.func,
+}
diff --git a/src/layout/EmailField.component.module.css b/src/layout/EmailField.component.module.css
new file mode 100644
index 00000000..41545276
--- /dev/null
+++ b/src/layout/EmailField.component.module.css
@@ -0,0 +1,16 @@
+.emailModalContainer {
+ margin-block-end: 8px;
+}
+
+.emailTextField {
+ width: 100%;
+}
+
+.emailModalItem {
+ margin-block-end: 16px;
+}
+
+.buttonContainer {
+ display: flex;
+ gap: 8px;
+}
\ No newline at end of file
diff --git a/src/layout/FormFields.component.js b/src/layout/FormFields.component.js
index 45f91315..8a213dda 100644
--- a/src/layout/FormFields.component.js
+++ b/src/layout/FormFields.component.js
@@ -15,8 +15,8 @@ import optionValueStore from '../optionValue.store.js'
import userSettingsStore from '../settings/userSettings.store.js'
import userSettingsKeyMapping from '../userSettingsMapping.js'
import AvatarEditor from './AvatarEditor.component.js'
+import { EmailField } from './EmailField.component.js'
import AppTheme from './theme.js'
-import { VerifyEmail } from './VerifyEmail.component.js'
import { VerifyEmailWarning } from './VerifyEmailWarning.js'
const styles = {
@@ -236,10 +236,17 @@ function createAvatarEditor(fieldBase, d2, valueStore) {
})
}
-function createVerifyButton(fieldBase, valueStore) {
+function createEmailField({ fieldBase, valueStore, onUpdate, d2 }) {
return Object.assign({}, fieldBase, {
- component: VerifyEmail,
- props: { userEmail: valueStore.state['email'] || '' },
+ component: EmailField,
+ props: {
+ onUpdate: (newEmail) => {
+ onUpdate('email', newEmail)
+ onUpdate('emailUpdated', true)
+ },
+ userEmail: valueStore.state['email'] || '',
+ userEmailVerified: d2?.currentUser?.emailVerified,
+ },
})
}
@@ -276,7 +283,7 @@ function createFieldBaseObject(fieldName, mapping, valueStore) {
)
}
-function createField(fieldName, valueStore, d2) {
+function createField({ fieldName, valueStore, d2, onUpdate }) {
const mapping = userSettingsKeyMapping[fieldName]
const fieldBase = createFieldBaseObject(fieldName, mapping, valueStore)
@@ -293,8 +300,13 @@ function createField(fieldName, valueStore, d2) {
return createAccountEditor(fieldBase, d2, valueStore)
case 'avatar':
return createAvatarEditor(fieldBase, d2, valueStore)
- case 'submit':
- return createVerifyButton(fieldBase, valueStore)
+ case 'emailModal':
+ return createEmailField({
+ fieldBase,
+ valueStore,
+ onUpdate,
+ d2,
+ })
default:
log.warn(
`Unknown control type "${mapping.type}" encountered for field "${fieldName}"`
@@ -365,9 +377,12 @@ class FormFields extends Component {
renderFields(fieldNames) {
const d2 = this.context.d2
const valueStore = this.props.valueStore
+ const onUpdate = this.props.onUpdateField
// Create the regular fields
const fields = fieldNames
- .map((fieldName) => createField(fieldName, valueStore, d2))
+ .map((fieldName) =>
+ createField({ fieldName, valueStore, d2, onUpdate })
+ )
.filter((field) => !!field.name)
.map((field) => wrapFieldWithLabel(field))
@@ -389,7 +404,15 @@ class FormFields extends Component {
{this.props.pageLabel}
diff --git a/src/layout/VerifyEmail.component.js b/src/layout/VerifyEmail.component.js
index 0abfd373..5865126a 100644
--- a/src/layout/VerifyEmail.component.js
+++ b/src/layout/VerifyEmail.component.js
@@ -1,5 +1,5 @@
import { useAlert, useDataMutation, useConfig } from '@dhis2/app-runtime'
-import { Button } from '@dhis2/ui'
+import { Button, email as emailValidator } from '@dhis2/ui'
import PropTypes from 'prop-types'
import React from 'react'
import i18n from '../locales/index.js'
@@ -9,9 +9,6 @@ const sendEmailVerificationMutation = {
type: 'create',
}
-const emailRegExp =
- /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
-
export function VerifyEmail({ userEmail }) {
const errorAlert = useAlert(({ message }) => message, { critical: true })
const successAlert = useAlert(({ message }) => message, { success: true })
@@ -37,23 +34,21 @@ export function VerifyEmail({ userEmail }) {
const emailConfigured = systemInfo?.emailConfigured
- const isValidEmail = emailRegExp.test(userEmail)
+ const isInvalidEmail = Boolean(emailValidator(userEmail))
if (!emailConfigured) {
return null
}
return (
-
-
- {i18n.t('Verify Email')}
-
-
+
+ {i18n.t('Verify email')}
+
)
}
diff --git a/src/layout/VerifyEmailWarning.js b/src/layout/VerifyEmailWarning.js
index 98199bf7..909c68e7 100644
--- a/src/layout/VerifyEmailWarning.js
+++ b/src/layout/VerifyEmailWarning.js
@@ -3,18 +3,23 @@ import { NoticeBox } from '@dhis2/ui'
import PropTypes from 'prop-types'
import React from 'react'
-export function VerifyEmailWarning({ config }) {
+export function VerifyEmailWarning({ config, emailUpdated, userEmail }) {
const enforceVerifiedEmail =
- config.system?.settings?.settings?.enforceVerifiedEmail || false
- const emailVerified = config.currentUser?.emailVerified || false
+ config.system?.settings?.settings?.enforceVerifiedEmail ?? false
+ const emailNotVerified =
+ (!config.currentUser?.emailVerified || emailUpdated) ?? false
- if (enforceVerifiedEmail && !emailVerified) {
+ if (enforceVerifiedEmail && emailNotVerified) {
return (
- {i18n.t(
- 'Your email is not verified. Please verify your email to continue using the system.'
- )}
+ {userEmail?.trim() !== ''
+ ? i18n.t(
+ 'Your email is not verified. Please verify your email to continue using the system.'
+ )
+ : i18n.t(
+ 'Please provide an email and verify it to continue using the system.'
+ )}
)
@@ -36,4 +41,6 @@ VerifyEmailWarning.propTypes = {
}),
}),
}).isRequired,
+ emailUpdated: PropTypes.bool,
+ userEmail: PropTypes.string,
}
diff --git a/src/profile/Profile.component.js b/src/profile/Profile.component.js
index 4664196f..3c0dc680 100644
--- a/src/profile/Profile.component.js
+++ b/src/profile/Profile.component.js
@@ -9,7 +9,6 @@ function EditProfile() {
'firstName',
'surname',
'email',
- 'emailVerification',
'avatar',
'phoneNumber',
'introduction',
diff --git a/src/userSettingsMapping.js b/src/userSettingsMapping.js
index cf1163a0..562cabfa 100644
--- a/src/userSettingsMapping.js
+++ b/src/userSettingsMapping.js
@@ -29,13 +29,7 @@ const settingsKeyMapping = {
},
email: {
label: i18n.t('E-mail'),
- type: 'textfield',
- validators: ['email'],
- },
- emailVerification: {
- name: 'emailVerification',
- label: i18n.t('E-mail Verification'),
- type: 'submit',
+ type: 'emailModal',
},
phoneNumber: {
label: i18n.t('Mobile phone number'),