diff --git a/web/modals/account/user-settings-modal.react.js b/web/modals/account/user-settings-modal.react.js index 1a347d140..92f706c19 100644 --- a/web/modals/account/user-settings-modal.react.js +++ b/web/modals/account/user-settings-modal.react.js @@ -1,523 +1,540 @@ // @flow import classNames from 'classnames'; import invariant from 'invariant'; import PropTypes from 'prop-types'; import * as React from 'react'; import { deleteAccountActionTypes, deleteAccount, changeUserSettingsActionTypes, changeUserSettings, resendVerificationEmailActionTypes, resendVerificationEmail, } from 'lib/actions/user-actions'; import { preRequestUserStateSelector } from 'lib/selectors/account-selectors'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors'; import { validEmailRegex } from 'lib/shared/account-utils'; import type { LogOutResult, ChangeUserSettingsResult, } from 'lib/types/account-types'; import { type PreRequestUserState, preRequestUserStatePropType, } from 'lib/types/session-types'; import { type AccountUpdate, type CurrentUserInfo, currentUserPropType, } from 'lib/types/user-types'; -import type { DispatchActionPromise } from 'lib/utils/action-utils'; -import { connect } from 'lib/utils/redux-utils'; +import { + type DispatchActionPromise, + useDispatchActionPromise, + useServerCall, +} from 'lib/utils/action-utils'; -import type { AppState } from '../../redux/redux-setup'; +import { useSelector } from '../../redux/redux-utils'; import css from '../../style.css'; import Modal from '../modal.react'; import VerifyEmailModal from './verify-email-modal.react'; type TabType = 'general' | 'delete'; type TabProps = { name: string, tabType: TabType, selected: boolean, onClick: (tabType: TabType) => void, }; class Tab extends React.PureComponent { render() { const classNamesForTab = classNames({ [css['current-tab']]: this.props.selected, [css['delete-tab']]: this.props.selected && this.props.tabType === 'delete', }); return (
  • {this.props.name}
  • ); } onClick = () => { return this.props.onClick(this.props.tabType); }; } -type Props = { - setModal: (modal: ?React.Node) => void, - // Redux state - currentUserInfo: ?CurrentUserInfo, - preRequestUserState: PreRequestUserState, - inputDisabled: boolean, - // Redux dispatch functions - dispatchActionPromise: DispatchActionPromise, - // async functions that hit server APIs - deleteAccount: ( +type BaseProps = {| + +setModal: (modal: ?React.Node) => void, +|}; +type Props = {| + ...BaseProps, + +currentUserInfo: ?CurrentUserInfo, + +preRequestUserState: PreRequestUserState, + +inputDisabled: boolean, + +dispatchActionPromise: DispatchActionPromise, + +deleteAccount: ( password: string, preRequestUserState: PreRequestUserState, ) => Promise, - changeUserSettings: ( + +changeUserSettings: ( accountUpdate: AccountUpdate, ) => Promise, - resendVerificationEmail: () => Promise, -}; + +resendVerificationEmail: () => Promise, +|}; type State = { email: ?string, emailVerified: ?boolean, newPassword: string, confirmNewPassword: string, currentPassword: string, errorMessage: string, currentTabType: TabType, }; class UserSettingsModal extends React.PureComponent { static propTypes = { setModal: PropTypes.func.isRequired, currentUserInfo: currentUserPropType, preRequestUserState: preRequestUserStatePropType.isRequired, inputDisabled: PropTypes.bool.isRequired, dispatchActionPromise: PropTypes.func.isRequired, deleteAccount: PropTypes.func.isRequired, changeUserSettings: PropTypes.func.isRequired, resendVerificationEmail: PropTypes.func.isRequired, }; emailInput: ?HTMLInputElement; newPasswordInput: ?HTMLInputElement; currentPasswordInput: ?HTMLInputElement; constructor(props: Props) { super(props); this.state = { email: this.email, emailVerified: this.emailVerified, newPassword: '', confirmNewPassword: '', currentPassword: '', errorMessage: '', currentTabType: 'general', }; } componentDidMount() { invariant(this.emailInput, 'email ref unset'); this.emailInput.focus(); } get username() { return this.props.currentUserInfo && !this.props.currentUserInfo.anonymous ? this.props.currentUserInfo.username : undefined; } get email() { return this.props.currentUserInfo && !this.props.currentUserInfo.anonymous ? this.props.currentUserInfo.email : undefined; } get emailVerified() { return this.props.currentUserInfo && !this.props.currentUserInfo.anonymous ? this.props.currentUserInfo.emailVerified : undefined; } render() { let mainContent = null; if (this.state.currentTabType === 'general') { let verificationStatus = null; if (this.state.emailVerified === true) { verificationStatus = (
    Verified
    ); } else if (this.state.emailVerified === false) { verificationStatus = (
    Not verified {' - '} resend verification email
    ); } mainContent = (
    Username
    {this.username}
    Email
    {verificationStatus}
    New password (optional)
    ); } else if (this.state.currentTabType === 'delete') { mainContent = (

    Your account will be permanently deleted. There is no way to reverse this.

    ); } let buttons = null; if (this.state.currentTabType === 'delete') { buttons = ( ); } else { buttons = ( ); } return (
    {mainContent}

    Please enter your current password to confirm your identity

    Current password
    {buttons}
    {this.state.errorMessage}
    ); } emailInputRef = (emailInput: ?HTMLInputElement) => { this.emailInput = emailInput; }; newPasswordInputRef = (newPasswordInput: ?HTMLInputElement) => { this.newPasswordInput = newPasswordInput; }; currentPasswordInputRef = (currentPasswordInput: ?HTMLInputElement) => { this.currentPasswordInput = currentPasswordInput; }; setTab = (tabType: TabType) => { this.setState({ currentTabType: tabType }); }; onChangeEmail = (event: SyntheticEvent) => { const target = event.target; invariant(target instanceof HTMLInputElement, 'target not input'); this.setState({ email: target.value, emailVerified: target.value === this.email ? this.emailVerified : null, }); }; onChangeNewPassword = (event: SyntheticEvent) => { const target = event.target; invariant(target instanceof HTMLInputElement, 'target not input'); this.setState({ newPassword: target.value }); }; onChangeConfirmNewPassword = (event: SyntheticEvent) => { const target = event.target; invariant(target instanceof HTMLInputElement, 'target not input'); this.setState({ confirmNewPassword: target.value }); }; onChangeCurrentPassword = (event: SyntheticEvent) => { const target = event.target; invariant(target instanceof HTMLInputElement, 'target not input'); this.setState({ currentPassword: target.value }); }; onClickResendVerificationEmail = ( event: SyntheticEvent, ) => { event.preventDefault(); this.props.dispatchActionPromise( resendVerificationEmailActionTypes, this.resendVerificationEmailAction(), ); }; async resendVerificationEmailAction() { await this.props.resendVerificationEmail(); this.props.setModal(); } onSubmit = (event: SyntheticEvent) => { event.preventDefault(); if (this.state.newPassword !== this.state.confirmNewPassword) { this.setState( { newPassword: '', confirmNewPassword: '', errorMessage: "passwords don't match", currentTabType: 'general', }, () => { invariant(this.newPasswordInput, 'newPasswordInput ref unset'); this.newPasswordInput.focus(); }, ); return; } const { email } = this.state; if (!email || email.search(validEmailRegex) === -1) { this.setState( { email: '', errorMessage: 'invalid email address', currentTabType: 'general', }, () => { invariant(this.emailInput, 'emailInput ref unset'); this.emailInput.focus(); }, ); return; } this.props.dispatchActionPromise( changeUserSettingsActionTypes, this.changeUserSettingsAction(email), ); }; async changeUserSettingsAction(email: string) { try { const result = await this.props.changeUserSettings({ updatedFields: { email, password: this.state.newPassword, }, currentPassword: this.state.currentPassword, }); if (email !== this.email) { this.props.setModal(); } else { this.clearModal(); } return result; } catch (e) { if (e.message === 'invalid_credentials') { this.setState( { currentPassword: '', errorMessage: 'wrong current password', }, () => { invariant( this.currentPasswordInput, 'currentPasswordInput ref unset', ); this.currentPasswordInput.focus(); }, ); } else if (e.message === 'email_taken') { this.setState( { email: this.email, emailVerified: this.emailVerified, errorMessage: 'email already taken', currentTabType: 'general', }, () => { invariant(this.emailInput, 'emailInput ref unset'); this.emailInput.focus(); }, ); } else { this.setState( { email: this.email, emailVerified: this.emailVerified, newPassword: '', confirmNewPassword: '', currentPassword: '', errorMessage: 'unknown error', currentTabType: 'general', }, () => { invariant(this.emailInput, 'emailInput ref unset'); this.emailInput.focus(); }, ); } throw e; } } onDelete = (event: SyntheticEvent) => { event.preventDefault(); this.props.dispatchActionPromise( deleteAccountActionTypes, this.deleteAction(), ); }; async deleteAction() { try { const response = await this.props.deleteAccount( this.state.currentPassword, this.props.preRequestUserState, ); this.clearModal(); return response; } catch (e) { const errorMessage = e.message === 'invalid_credentials' ? 'wrong password' : 'unknown error'; this.setState( { currentPassword: '', errorMessage: errorMessage, }, () => { invariant( this.currentPasswordInput, 'currentPasswordInput ref unset', ); this.currentPasswordInput.focus(); }, ); throw e; } } clearModal = () => { this.props.setModal(null); }; } const deleteAccountLoadingStatusSelector = createLoadingStatusSelector( deleteAccountActionTypes, ); const changeUserSettingsLoadingStatusSelector = createLoadingStatusSelector( changeUserSettingsActionTypes, ); const resendVerificationEmailLoadingStatusSelector = createLoadingStatusSelector( resendVerificationEmailActionTypes, ); -export default connect( - (state: AppState) => ({ - currentUserInfo: state.currentUserInfo, - preRequestUserState: preRequestUserStateSelector(state), - inputDisabled: +export default React.memo(function ConnectedUserSettingsModal( + props: BaseProps, +) { + const currentUserInfo = useSelector((state) => state.currentUserInfo); + const preRequestUserState = useSelector(preRequestUserStateSelector); + const inputDisabled = useSelector( + (state) => deleteAccountLoadingStatusSelector(state) === 'loading' || changeUserSettingsLoadingStatusSelector(state) === 'loading' || resendVerificationEmailLoadingStatusSelector(state) === 'loading', - }), - { - deleteAccount, - changeUserSettings, - resendVerificationEmail, - }, -)(UserSettingsModal); + ); + const callDeleteAccount = useServerCall(deleteAccount); + const callChangeUserSettings = useServerCall(changeUserSettings); + const callResendVerificationEmail = useServerCall(resendVerificationEmail); + const dispatchActionPromise = useDispatchActionPromise(); + + return ( + + ); +});