Page MenuHomePhabricator

D4117.id13083.diff
No OneTemporary

D4117.id13083.diff

diff --git a/web/settings/account-delete-modal.css b/web/settings/account-delete-modal.css
new file mode 100644
--- /dev/null
+++ b/web/settings/account-delete-modal.css
@@ -0,0 +1,143 @@
+div.modal-body {
+ padding: 6px 6px;
+ width: 100%;
+ box-sizing: border-box;
+ background-color: var(--modal-bg);
+ border-bottom-left-radius: 15px;
+ border-bottom-right-radius: 15px;
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+}
+div.resized-modal-body {
+ min-height: 250px;
+}
+div.modal-body p {
+ padding: 1px 3px 4px 3px;
+ font-size: 14px;
+ text-align: center;
+}
+div.modal-body p.form-pre-footer {
+ padding-top: 5px;
+ font-size: 12px;
+ font-style: italic;
+}
+div.modal-body textarea {
+ margin: 3px;
+}
+div.modal-body textarea {
+ font-size: 14px;
+ padding: 1px;
+ width: 175px;
+}
+div.large-modal-container div.modal-body textarea {
+ width: 275px;
+}
+div.modal-body p.confirm-account-password {
+ margin-bottom: 4px;
+ color: var(--fg);
+}
+div.modal-body div.form-footer {
+ display: flex;
+ flex-direction: row-reverse;
+ justify-content: space-between;
+ padding-top: 8px;
+}
+div.modal-body div.form-footer div.modal-form-error {
+ font-size: 12px;
+ color: red;
+ font-style: italic;
+ padding-left: 6px;
+ align-self: center;
+}
+div.modal-body div.form-footer div.modal-form-error ol {
+ padding-left: 20px;
+}
+div.modal-body div.form-title {
+ display: inline-block;
+ text-align: right;
+ padding-right: 5px;
+ padding-top: 5px;
+ font-size: 14px;
+ font-weight: 600;
+ vertical-align: top;
+ width: 110px;
+ color: var(--fg);
+}
+div.large-modal-container div.modal-body div.form-title {
+ width: 140px;
+}
+div.modal-body div.form-content {
+ display: inline-block;
+ font-family: var(--font-stack);
+ color: var(--fg);
+}
+div.modal-body div.form-content input {
+ margin-bottom: 4px;
+}
+div.modal-body div.form-subtitle {
+ font-size: 12px;
+ padding-left: 4px;
+ font-style: italic;
+}
+
+div.form-text {
+ display: flex;
+ align-items: baseline;
+}
+div.form-text > div.form-title {
+ vertical-align: initial;
+ flex-shrink: 0;
+}
+div.form-text > div.form-content {
+ margin-left: 3px;
+ margin-bottom: 3px;
+ word-break: break-word;
+}
+
+div.form-text > div.form-float-title {
+ float: left;
+ text-align: right;
+ padding-right: 5px;
+ font-size: 14px;
+ font-weight: 600;
+ width: 110px;
+}
+div.form-text > div.form-float-content {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ font-size: 14px;
+ padding: 1px 20px 3px 4px;
+ margin-top: 5px;
+}
+
+.italic {
+ font-style: italic;
+ color: var(--fg);
+}
+
+ul.tab-panel {
+ background-color: var(--modal-bg);
+ padding-left: 10px;
+ padding-top: 5px;
+}
+ul.tab-panel > li {
+ display: inline-block;
+ list-style-type: none;
+ font-size: 13px;
+ font-weight: 600;
+ cursor: pointer;
+ padding: 3px 10px 3px 10px;
+}
+ul.tab-panel > li > a {
+ color: #555555;
+}
+ul.tab-panel > li.delete-tab > a {
+ color: #ff0000 !important;
+}
+
+div.user-settings-current-password {
+ padding-top: 4px;
+ margin-top: 5px;
+}
diff --git a/web/settings/account-delete-modal.react.js b/web/settings/account-delete-modal.react.js
new file mode 100644
--- /dev/null
+++ b/web/settings/account-delete-modal.react.js
@@ -0,0 +1,440 @@
+// @flow
+
+import classNames from 'classnames';
+import invariant from 'invariant';
+import * as React from 'react';
+
+import {
+ deleteAccountActionTypes,
+ deleteAccount,
+ changeUserPasswordActionTypes,
+ changeUserPassword,
+ logOut,
+ logOutActionTypes,
+} from 'lib/actions/user-actions';
+import { preRequestUserStateSelector } from 'lib/selectors/account-selectors';
+import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors';
+import type { LogOutResult } from 'lib/types/account-types';
+import { type PreRequestUserState } from 'lib/types/session-types';
+import {
+ type PasswordUpdate,
+ type CurrentUserInfo,
+} from 'lib/types/user-types';
+import {
+ type DispatchActionPromise,
+ useDispatchActionPromise,
+ useServerCall,
+} from 'lib/utils/action-utils';
+
+import Button from '../components/button.react';
+import Input from '../modals/input.react';
+import { useModalContext } from '../modals/modal-provider.react';
+import Modal from '../modals/modal.react';
+import { useSelector } from '../redux/redux-utils';
+import css from './account-delete-modal.css';
+
+type TabType = 'general' | 'delete';
+type TabProps = {
+ +name: string,
+ +tabType: TabType,
+ +selected: boolean,
+ +onClick: (tabType: TabType) => void,
+};
+class Tab extends React.PureComponent<TabProps> {
+ render() {
+ const { selected, name, tabType } = this.props;
+ const classNamesForTab = classNames({
+ [css['current-tab']]: selected,
+ [css['delete-tab']]: selected && tabType === 'delete',
+ });
+ return (
+ <li className={classNamesForTab} onClick={this.onClick}>
+ <a>{name}</a>
+ </li>
+ );
+ }
+
+ onClick = () => {
+ return this.props.onClick(this.props.tabType);
+ };
+}
+
+type Props = {
+ +currentUserInfo: ?CurrentUserInfo,
+ +preRequestUserState: PreRequestUserState,
+ +inputDisabled: boolean,
+ +dispatchActionPromise: DispatchActionPromise,
+ +deleteAccount: (
+ password: string,
+ preRequestUserState: PreRequestUserState,
+ ) => Promise<LogOutResult>,
+ +changeUserPassword: (passwordUpdate: PasswordUpdate) => Promise<void>,
+ +logOut: (preRequestUserState: PreRequestUserState) => Promise<LogOutResult>,
+ +popModal: () => void,
+};
+type State = {
+ +newPassword: string,
+ +confirmNewPassword: string,
+ +currentPassword: string,
+ +errorMessage: string,
+ +currentTabType: TabType,
+};
+
+class AccountDeleteModal extends React.PureComponent<Props, State> {
+ newPasswordInput: ?HTMLInputElement;
+ currentPasswordInput: ?HTMLInputElement;
+
+ constructor(props: Props) {
+ super(props);
+ this.state = {
+ newPassword: '',
+ confirmNewPassword: '',
+ currentPassword: '',
+ errorMessage: '',
+ currentTabType: 'general',
+ };
+ }
+
+ componentDidMount() {
+ invariant(this.newPasswordInput, 'newPasswordInput ref unset');
+ this.newPasswordInput.focus();
+ }
+
+ get username() {
+ return this.props.currentUserInfo && !this.props.currentUserInfo.anonymous
+ ? this.props.currentUserInfo.username
+ : undefined;
+ }
+
+ onLogOut = (event: SyntheticEvent<HTMLButtonElement>) => {
+ event.preventDefault();
+ this.props.dispatchActionPromise(logOutActionTypes, this.logOut());
+ };
+
+ logOut = async () => {
+ await this.props.logOut(this.props.preRequestUserState);
+ this.props.popModal();
+ };
+
+ render() {
+ const { inputDisabled } = this.props;
+ let mainContent = null;
+ if (this.state.currentTabType === 'general') {
+ mainContent = (
+ <div>
+ <div className={css['form-text']}>
+ <div className={css['form-title']}>Username</div>
+ <div className={css['form-content']}>{this.username}</div>
+ </div>
+ <div>
+ <div className={css['form-title']}>New password</div>
+ <div className={css['form-content']}>
+ <div>
+ <Input
+ type="password"
+ placeholder="New password"
+ value={this.state.newPassword}
+ onChange={this.onChangeNewPassword}
+ ref={this.newPasswordInputRef}
+ disabled={inputDisabled}
+ />
+ </div>
+ <div>
+ <Input
+ type="password"
+ placeholder="Confirm new password"
+ value={this.state.confirmNewPassword}
+ onChange={this.onChangeConfirmNewPassword}
+ disabled={inputDisabled}
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ } else if (this.state.currentTabType === 'delete') {
+ mainContent = (
+ <p className={css['italic']}>
+ Your account will be permanently deleted. There is no way to reverse
+ this.
+ </p>
+ );
+ }
+
+ let buttons = null;
+ if (this.state.currentTabType === 'delete') {
+ buttons = (
+ <Button
+ variant="danger"
+ type="submit"
+ onClick={this.onDelete}
+ disabled={inputDisabled}
+ >
+ Delete account
+ </Button>
+ );
+ } else {
+ buttons = (
+ <>
+ <Button
+ type="submit"
+ variant="primary"
+ onClick={this.onSubmit}
+ disabled={inputDisabled}
+ >
+ Update Account
+ </Button>
+ <Button
+ type="submit"
+ variant="secondary"
+ onClick={this.onLogOut}
+ disabled={inputDisabled}
+ >
+ Log out
+ </Button>
+ </>
+ );
+ }
+
+ let errorMsg;
+ if (this.state.errorMessage) {
+ errorMsg = (
+ <div className={css['modal-form-error']}>{this.state.errorMessage}</div>
+ );
+ }
+
+ return (
+ <Modal name="Edit account" onClose={this.props.popModal} size="large">
+ <ul className={css['tab-panel']}>
+ <Tab
+ name="General"
+ tabType="general"
+ onClick={this.setTab}
+ selected={this.state.currentTabType === 'general'}
+ key="general"
+ />
+ <Tab
+ name="Delete"
+ tabType="delete"
+ onClick={this.setTab}
+ selected={this.state.currentTabType === 'delete'}
+ key="delete"
+ />
+ </ul>
+ <div className={css['modal-body']}>
+ <form method="POST">
+ {mainContent}
+ <div className={css['user-settings-current-password']}>
+ <p className={css['confirm-account-password']}>
+ Please enter your current password to confirm your identity
+ </p>
+ <div className={css['form-title']}>Current password</div>
+ <div className={css['form-content']}>
+ <Input
+ type="password"
+ placeholder="Current password"
+ value={this.state.currentPassword}
+ onChange={this.onChangeCurrentPassword}
+ disabled={inputDisabled}
+ ref={this.currentPasswordInputRef}
+ />
+ </div>
+ </div>
+ <div className={css['form-footer']}>
+ {buttons}
+ {errorMsg}
+ </div>
+ </form>
+ </div>
+ </Modal>
+ );
+ }
+
+ newPasswordInputRef = (newPasswordInput: ?HTMLInputElement) => {
+ this.newPasswordInput = newPasswordInput;
+ };
+
+ currentPasswordInputRef = (currentPasswordInput: ?HTMLInputElement) => {
+ this.currentPasswordInput = currentPasswordInput;
+ };
+
+ setTab = (tabType: TabType) => {
+ this.setState({ currentTabType: tabType });
+ };
+
+ onChangeNewPassword = (event: SyntheticEvent<HTMLInputElement>) => {
+ const target = event.target;
+ invariant(target instanceof HTMLInputElement, 'target not input');
+ this.setState({ newPassword: target.value });
+ };
+
+ onChangeConfirmNewPassword = (event: SyntheticEvent<HTMLInputElement>) => {
+ const target = event.target;
+ invariant(target instanceof HTMLInputElement, 'target not input');
+ this.setState({ confirmNewPassword: target.value });
+ };
+
+ onChangeCurrentPassword = (event: SyntheticEvent<HTMLInputElement>) => {
+ const target = event.target;
+ invariant(target instanceof HTMLInputElement, 'target not input');
+ this.setState({ currentPassword: target.value });
+ };
+
+ onSubmit = (event: SyntheticEvent<HTMLButtonElement>) => {
+ event.preventDefault();
+
+ if (this.state.newPassword === '') {
+ this.setState(
+ {
+ newPassword: '',
+ confirmNewPassword: '',
+ errorMessage: 'empty password',
+ currentTabType: 'general',
+ },
+ () => {
+ invariant(this.newPasswordInput, 'newPasswordInput ref unset');
+ this.newPasswordInput.focus();
+ },
+ );
+ } else 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;
+ }
+
+ this.props.dispatchActionPromise(
+ changeUserPasswordActionTypes,
+ this.changeUserSettingsAction(),
+ );
+ };
+
+ async changeUserSettingsAction() {
+ try {
+ await this.props.changeUserPassword({
+ updatedFields: {
+ password: this.state.newPassword,
+ },
+ currentPassword: this.state.currentPassword,
+ });
+ this.props.popModal();
+ } catch (e) {
+ if (e.message === 'invalid_credentials') {
+ this.setState(
+ {
+ currentPassword: '',
+ errorMessage: 'wrong current password',
+ },
+ () => {
+ invariant(
+ this.currentPasswordInput,
+ 'currentPasswordInput ref unset',
+ );
+ this.currentPasswordInput.focus();
+ },
+ );
+ } else {
+ this.setState(
+ {
+ newPassword: '',
+ confirmNewPassword: '',
+ currentPassword: '',
+ errorMessage: 'unknown error',
+ currentTabType: 'general',
+ },
+ () => {
+ invariant(this.newPasswordInput, 'newPasswordInput ref unset');
+ this.newPasswordInput.focus();
+ },
+ );
+ }
+ throw e;
+ }
+ }
+
+ onDelete = (event: SyntheticEvent<HTMLButtonElement>) => {
+ event.preventDefault();
+ this.props.dispatchActionPromise(
+ deleteAccountActionTypes,
+ this.deleteAction(),
+ );
+ };
+
+ async deleteAction() {
+ try {
+ const response = await this.props.deleteAccount(
+ this.state.currentPassword,
+ this.props.preRequestUserState,
+ );
+ this.props.popModal();
+ 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;
+ }
+ }
+}
+
+const deleteAccountLoadingStatusSelector = createLoadingStatusSelector(
+ deleteAccountActionTypes,
+);
+const changeUserPasswordLoadingStatusSelector = createLoadingStatusSelector(
+ changeUserPasswordActionTypes,
+);
+const ConnectedAccountDeleteModal: React.ComponentType<{}> = React.memo<{}>(
+ function ConnectedAccountDeleteModal(): React.Node {
+ const currentUserInfo = useSelector(state => state.currentUserInfo);
+ const preRequestUserState = useSelector(preRequestUserStateSelector);
+ const inputDisabled = useSelector(
+ state =>
+ deleteAccountLoadingStatusSelector(state) === 'loading' ||
+ changeUserPasswordLoadingStatusSelector(state) === 'loading',
+ );
+ const callDeleteAccount = useServerCall(deleteAccount);
+ const callChangeUserPassword = useServerCall(changeUserPassword);
+ const dispatchActionPromise = useDispatchActionPromise();
+ const boundLogOut = useServerCall(logOut);
+
+ const modalContext = useModalContext();
+
+ return (
+ <AccountDeleteModal
+ currentUserInfo={currentUserInfo}
+ preRequestUserState={preRequestUserState}
+ inputDisabled={inputDisabled}
+ deleteAccount={callDeleteAccount}
+ changeUserPassword={callChangeUserPassword}
+ dispatchActionPromise={dispatchActionPromise}
+ logOut={boundLogOut}
+ popModal={modalContext.popModal}
+ />
+ );
+ },
+);
+
+export default ConnectedAccountDeleteModal;

File Metadata

Mime Type
text/plain
Expires
Thu, Nov 28, 5:27 PM (21 h, 35 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2594379
Default Alt Text
D4117.id13083.diff (16 KB)

Event Timeline