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

Feature/change email #407

Merged
merged 4 commits into from
Nov 5, 2024
Merged
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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ For detailed rules of this file, see [Changelog Rules](#changelog-rules)

### ✨ Added

*
* An option for users to update their registered email address through the user profile.

### ⚡ Changed

Expand Down
42 changes: 42 additions & 0 deletions src/components/ChangeEmailModal/ChangeEmailMessageBox.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import PropTypes from 'prop-types'

import ApiErrorBox from '~/components/MessageBox/ApiErrorBox'

import MessageBox from '../MessageBox'





function getErrorText (error) {
switch (error.status) {
default:
return undefined
}
}

function ChangeEmailMessageBox (props) {
const { result } = props

return result.success
? (
<MessageBox type="success">
{'E-Mail changed! Please login to continue.'}
</MessageBox>
)
: (
<ApiErrorBox
error={result.error}
renderError={getErrorText} />
)
}

ChangeEmailMessageBox.propTypes = {
result: PropTypes.object,
}





export default ChangeEmailMessageBox
127 changes: 127 additions & 0 deletions src/components/ChangeEmailModal/ChangeEmailModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import Router from 'next/router'
import PropTypes from 'prop-types'
import { useCallback, useMemo, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'

import asModal, { ModalContent, ModalFooter } from '~/components/asModal'
import EmailFieldset from '~/components/Fieldsets/EmailFieldset'
import useForm from '~/hooks/useForm'
import { logout } from '~/store/actions/session'
import { changeEmail } from '~/store/actions/user'
import {
selectCurrentUserId,
selectUserById,
withCurrentUserId,
} from '~/store/selectors'
import getResponseError from '~/util/getResponseError'

import ChangeEmailMessageBox from './ChangeEmailMessageBox'

// Component Constants
const SUBMIT_AUTO_CLOSE_DELAY_TIME = 3000





function ChangeEmailModal (props) {
const {
onClose,
isOpen,
} = props

const [result, setResult] = useState({})
const { email } = useSelector(withCurrentUserId(selectUserById))?.attributes ?? {}

const dispatch = useDispatch()
const onSubmit = useCallback(async (formData) => {
if (result.submitted) {
setResult({ submitted: true })
}
const response = await dispatch(changeEmail(formData))

const error = getResponseError(response)

setResult({
error,
success: !error,
submitted: true,
})

if (!error) {
setTimeout(() => {
if (isOpen) {
onClose()
}
}, SUBMIT_AUTO_CLOSE_DELAY_TIME)

await dispatch(logout())
Router.reload()
}
}, [dispatch, isOpen, onClose, result.submitted])


const userId = useSelector(selectCurrentUserId)
const data = useMemo(() => {
return {
id: userId,
attributes: {
email: '',
},
}
}, [userId])

const { Form, submitting, canSubmit } = useForm({ data, onSubmit })

return (
<ModalContent as={Form} className="dialog no-pad">
<ChangeEmailMessageBox result={result} />

{
!result.submitted && (
<div className="info">
<div className="email">
<span className="label">{'Current E-Mail: '}</span>
<span>{email}</span>
</div>
{'Enter the new e-mail address you would like to associate with your account below.'}
</div>
)
}

<EmailFieldset
dark
required
aria-label="New E-Mail Address"
id="NewEmailAddress"
name="attributes.email"
placeholder="New E-Mail Address" />

<ModalFooter>
<div className="secondary" />
<div className="primary">
<button
className="green"
disabled={!canSubmit || submitting || result.success}
type="submit">
{submitting ? 'Submitting...' : 'Change E-Mail'}
</button>
</div>
</ModalFooter>
</ModalContent>
)
}

ChangeEmailModal.propTypes = {
isOpen: PropTypes.any,
onClose: PropTypes.func.isRequired,
}





export default asModal({
className: 'email-change-dialog',
title: 'Change E-Mail Address',
})(ChangeEmailModal)
1 change: 1 addition & 0 deletions src/components/ChangeEmailModal/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './ChangeEmailModal'
16 changes: 16 additions & 0 deletions src/components/ProfileHeader/ProfileHeader.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from '~/store/selectors'
import formatAsEliteDateTime from '~/util/date/formatAsEliteDateTime'

import ChangeEmailModal from '../ChangeEmailModal'
import ChangePasswordModal from '../ChangePasswordModal'
import DisableProfileModal from '../DisableProfileModal'
import ProfileUserAvatar from '../ProfileUserAvatar'
Expand All @@ -21,9 +22,16 @@ import UnverifiedUserBanner from './UnverifiedUserBanner'


function ProfileHeader () {
const [showChangeEmail, setShowChangeEmail] = useState(false)
const [showChangePassword, setShowChangePassword] = useState(false)
const [showDisableProfile, setShowDisableProfile] = useState(false)

const handleToggleChangeEmail = useCallback(() => {
setShowChangeEmail((state) => {
return !state
})
}, [])

const handleToggleChangePassword = useCallback(() => {
setShowChangePassword((state) => {
return !state
Expand Down Expand Up @@ -77,6 +85,11 @@ function ProfileHeader () {
</ul>
</div>
<div className="profile-controls">
<button
type="button"
onClick={handleToggleChangeEmail}>
{'Change E-Mail'}
</button>
<button
type="button"
onClick={handleToggleChangePassword}>
Expand All @@ -89,6 +102,9 @@ function ProfileHeader () {
</button>
</div>
</div>
<ChangeEmailModal
isOpen={showChangeEmail}
onClose={handleToggleChangeEmail} />
<ChangePasswordModal
isOpen={showChangePassword}
onClose={handleToggleChangePassword} />
Expand Down
3 changes: 3 additions & 0 deletions src/store/actionTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ const users = {
avatar: {
update: 'users/avatar/update',
},
email: {
update: 'users/email/update',
},
}


Expand Down
2 changes: 0 additions & 2 deletions src/store/actions/authentication.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ const SESSION_TOKEN_LENGTH = 365 // days





export const changePassword = ({ id, ...data }) => {
return frApiPlainRequest(
actionTypes.passwords.update,
Expand Down
14 changes: 12 additions & 2 deletions src/store/actions/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import createRequestBody from '~/util/jsonapi/createRequestBody'
import actionTypes from '../actionTypes'
import { deletesResource, deletesRelationship, createsRelationship, RESOURCE } from '../reducers/frAPIResources'
import { withCurrentUserId, selectUserById, selectCurrentUserId } from '../selectors'
import { frApiRequest } from './services'

import { frApiRequest, frApiPlainRequest } from './services'

export const getNickname = (nickId) => {
return frApiRequest(
Expand Down Expand Up @@ -93,3 +92,14 @@ export const updateAvatar = (data) => {
return dispatch(frApiRequest(actionTypes.users.avatar.update, request))
}
}

export const changeEmail = ({ id, ...data }) => {
return frApiPlainRequest(
actionTypes.users.email.update,
{
url: `/users/${id}/email`,
method: 'patch',
data: createRequestBody('email-changes', data),
},
)
}