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?')}
+
+ + + + + + + + +
+) + +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} + /> + + + + + + + + + + + + + ) +} + +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 ( +
+ +
+ + + + + +
+ {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}
{this.context?.d2 && ( - + )} {this.renderFields(this.props.fieldKeys)} 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 ( -
- -
+ ) } 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'),