Implemented handlers to delete/restore account. Implemented "account deleted" page

This commit is contained in:
ErickSkrauch 2020-07-27 10:28:37 +03:00
parent 9247ad7178
commit c0b3e328b6
No known key found for this signature in database
GPG Key ID: 669339FCBB30EE0E
9 changed files with 137 additions and 11 deletions

View File

@ -0,0 +1,13 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { ProfileLayout } from 'app/components/profile/Profile.story';
import AccountDeleted from './AccountDeleted';
storiesOf('Components/Profile', module).add('AccountDeleted', () => (
<ProfileLayout>
<AccountDeleted onRestore={action('onRestore')} />
</ProfileLayout>
));

View File

@ -0,0 +1,48 @@
import React, { ComponentType } from 'react';
import { FormattedMessage as Message } from 'react-intl';
import { Helmet } from 'react-helmet-async';
import { Button } from 'app/components/ui/form';
import styles from './accountDeleted.scss';
interface Props {
onRestore?: () => void;
}
const AccountDeleted: ComponentType<Props> = ({ onRestore }) => {
return (
<div className={styles.wrapper}>
<Message key="accountDeleted" defaultMessage="Account deleted">
{(pageTitle: string) => (
<h2 className={styles.title}>
<Helmet title={pageTitle} />
{pageTitle}
</h2>
)}
</Message>
<div className={styles.description}>
<Message
key="accountDeletedDescription"
// TODO: verify translation
defaultMessage="The account has been marked for deletion and will be permanently removed within a week. Until then, all activity on the account has been suspended."
/>
</div>
<div className={styles.description}>
<Message
key="ifYouWantToRestoreAccount"
// TODO: verify translation
defaultMessage="If you want to restore your account, click on the button below."
/>
</div>
<Button onClick={onRestore} color="black" small>
<Message key="restoreAccount" defaultMessage="Restore account" />
</Button>
</div>
);
};
export default AccountDeleted;

View File

@ -22,6 +22,7 @@ storiesOf('Components/Profile', module).add('Profile', () => (
hasMojangUsernameCollision: true, hasMojangUsernameCollision: true,
isActive: true, isActive: true,
isGuest: false, isGuest: false,
isDeleted: false,
isOtpEnabled: true, isOtpEnabled: true,
lang: 'unknown', lang: 'unknown',
passwordChangedAt: 1595328712, passwordChangedAt: 1595328712,

View File

@ -0,0 +1,24 @@
.wrapper {
text-align: center;
@media (min-height: 600px) {
margin-top: 140px;
}
}
.title {
composes: indexTitle from '~app/components/profile/profile.scss';
margin-bottom: 25px;
}
.description {
composes: indexDescription from '~app/components/profile/profile.scss';
margin: 0 auto 20px auto;
max-width: 330px;
&:last-of-type {
margin-bottom: 25px;
}
}

View File

@ -10,6 +10,7 @@ export interface User {
lang: string; lang: string;
isGuest: boolean; isGuest: boolean;
isActive: boolean; isActive: boolean;
isDeleted: boolean;
isOtpEnabled: boolean; isOtpEnabled: boolean;
passwordChangedAt: number; passwordChangedAt: number;
hasMojangUsernameCollision: boolean; hasMojangUsernameCollision: boolean;
@ -31,6 +32,7 @@ const defaults: State = {
avatar: '', avatar: '',
lang: '', lang: '',
isActive: false, isActive: false,
isDeleted: false,
isOtpEnabled: false, isOtpEnabled: false,
shouldAcceptRules: false, // whether user need to review updated rules shouldAcceptRules: false, // whether user need to review updated rules
passwordChangedAt: 0, passwordChangedAt: 0,

View File

@ -0,0 +1,26 @@
import React, { ComponentType, useCallback, useContext } from 'react';
import { useReduxDispatch } from 'app/functions';
import { restoreAccount } from 'app/services/api/accounts';
import { updateUser } from 'app/components/user/actions';
import ProfileContext from 'app/components/profile/Context';
import AccountDeleted from 'app/components/profile/AccountDeleted';
const AccountDeletedPage: ComponentType = () => {
const dispatch = useReduxDispatch();
const context = useContext(ProfileContext);
const onRestore = useCallback(async () => {
await restoreAccount(context.userId);
dispatch(
updateUser({
isDeleted: false,
}),
);
context.goToProfile();
}, [dispatch, context]);
return <AccountDeleted onRestore={onRestore} />;
};
export default AccountDeletedPage;

View File

@ -1,7 +1,7 @@
import React, { ComponentType, useCallback, useContext, useRef } from 'react'; import React, { ComponentType, useCallback, useContext, useRef } from 'react';
import { useReduxDispatch } from 'app/functions'; import { useReduxDispatch } from 'app/functions';
import { changePassword } from 'app/services/api/accounts'; import { deleteAccount } from 'app/services/api/accounts';
import { FormModel } from 'app/components/ui/form'; import { FormModel } from 'app/components/ui/form';
import DeleteAccount from 'app/components/profile/deleteAccount'; import DeleteAccount from 'app/components/profile/deleteAccount';
import { updateUser } from 'app/components/user/actions'; import { updateUser } from 'app/components/user/actions';
@ -16,13 +16,12 @@ const DeleteAccountPage: ComponentType = () => {
context context
.onSubmit({ .onSubmit({
form, form,
// TODO: rework sendData: () => deleteAccount(context.userId, form.serialize()),
sendData: () => changePassword(context.userId, form.serialize()),
}) })
.then(() => { .then(() => {
dispatch( dispatch(
updateUser({ updateUser({
passwordChangedAt: Date.now() / 1000, isDeleted: true,
}), }),
); );
context.goToProfile(); context.goToProfile();

View File

@ -11,7 +11,7 @@ import { browserHistory } from 'app/services/history';
import { FooterMenu } from 'app/components/footerMenu'; import { FooterMenu } from 'app/components/footerMenu';
import { FormModel } from 'app/components/ui/form'; import { FormModel } from 'app/components/ui/form';
import { Provider } from 'app/components/profile/Context'; import { Provider } from 'app/components/profile/Context';
import { ComponentLoader } from 'app/components/ui/loader'; import { User } from 'app/components/user';
import styles from './profile.scss'; import styles from './profile.scss';
@ -21,14 +21,15 @@ import ChangeUsernamePage from 'app/pages/profile/ChangeUsernamePage';
import ChangeEmailPage from 'app/pages/profile/ChangeEmailPage'; import ChangeEmailPage from 'app/pages/profile/ChangeEmailPage';
import MultiFactorAuthPage from 'app/pages/profile/MultiFactorAuthPage'; import MultiFactorAuthPage from 'app/pages/profile/MultiFactorAuthPage';
import DeleteAccountPage from 'app/pages/profile/DeleteAccountPage'; import DeleteAccountPage from 'app/pages/profile/DeleteAccountPage';
import AccountDeletedPage from 'app/pages/profile/AccountDeletedPage';
interface Props { interface Props {
userId: number; user: User;
onSubmit: (options: { form: FormModel; sendData: () => Promise<any> }) => Promise<void>; onSubmit: (options: { form: FormModel; sendData: () => Promise<any> }) => Promise<void>;
refreshUserData: () => Promise<any>; refreshUserData: () => Promise<any>;
} }
const ProfileController: ComponentType<Props> = ({ userId, onSubmit, refreshUserData }) => { const ProfileController: ComponentType<Props> = ({ user, onSubmit, refreshUserData }) => {
const goToProfile = useCallback(async () => { const goToProfile = useCallback(async () => {
await refreshUserData(); await refreshUserData();
@ -39,12 +40,14 @@ const ProfileController: ComponentType<Props> = ({ userId, onSubmit, refreshUser
<div className={styles.container}> <div className={styles.container}>
<Provider <Provider
value={{ value={{
userId, userId: user.id!,
onSubmit, onSubmit,
goToProfile, goToProfile,
}} }}
> >
<React.Suspense fallback={<ComponentLoader />}> {user.isDeleted ? (
<AccountDeletedPage />
) : (
<Switch> <Switch>
<Route path="/profile/mfa/step:step([1-3])" component={MultiFactorAuthPage} /> <Route path="/profile/mfa/step:step([1-3])" component={MultiFactorAuthPage} />
<Route path="/profile/mfa" exact component={MultiFactorAuthPage} /> <Route path="/profile/mfa" exact component={MultiFactorAuthPage} />
@ -56,7 +59,7 @@ const ProfileController: ComponentType<Props> = ({ userId, onSubmit, refreshUser
<Route path="/" exact component={Profile} /> <Route path="/" exact component={Profile} />
<Redirect to="/404" /> <Redirect to="/404" />
</Switch> </Switch>
</React.Suspense> )}
<div className={styles.footer}> <div className={styles.footer}>
<FooterMenu /> <FooterMenu />
@ -68,7 +71,7 @@ const ProfileController: ComponentType<Props> = ({ userId, onSubmit, refreshUser
export default connect( export default connect(
(state) => ({ (state) => ({
userId: state.user.id!, user: state.user,
}), }),
{ {
refreshUserData, refreshUserData,

View File

@ -86,3 +86,13 @@ export function confirmNewEmail(id: number, key: string): Promise<{ success: boo
key, key,
}); });
} }
export function deleteAccount(id: number, { password }: { password?: string }): Promise<{ success: boolean }> {
return request.delete(`/api/v1/accounts/${id}`, {
password,
});
}
export function restoreAccount(id: number): Promise<{ success: boolean }> {
return request.post(`/api/v1/accounts/${id}/restore`);
}