Skip to content

Commit

Permalink
feat(user): initial account deletion implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
naumovski-filip committed May 22, 2023
1 parent ba11ebe commit b6eda04
Show file tree
Hide file tree
Showing 17 changed files with 320 additions and 24 deletions.
2 changes: 1 addition & 1 deletion src/components/Account/Account.module.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.exportDataContainer {
.textWithButtonContainer {
display: flex;
flex-direction: column;
gap: 1rem;
Expand Down
29 changes: 26 additions & 3 deletions src/components/Account/Account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,14 @@ const Account = ({ panelClassName, panelHeaderClassName, canUpdateEmail = true }
}
}, [exportData.isSuccess, exportData.isError]);

const { customer, customerConsents, publisherConsents, canChangePasswordWithOldPassword, canExportAccountData } = useAccountStore(
({ user, customerConsents, publisherConsents, canChangePasswordWithOldPassword, canExportAccountData }) => ({
const { customer, customerConsents, publisherConsents, canChangePasswordWithOldPassword, canExportAccountData, canDeleteAccount } = useAccountStore(
({ user, customerConsents, publisherConsents, canChangePasswordWithOldPassword, canExportAccountData, canDeleteAccount }) => ({
customer: user,
customerConsents,
publisherConsents,
canChangePasswordWithOldPassword,
canExportAccountData,
canDeleteAccount: canDeleteAccount,
}),
shallow,
);
Expand Down Expand Up @@ -269,7 +270,7 @@ const Account = ({ panelClassName, panelHeaderClassName, canUpdateEmail = true }
formSection({
label: t('account.export_data_title'),
content: (section) => (
<div className={styles.exportDataContainer}>
<div className={styles.textWithButtonContainer}>
<div>
<Trans t={t} i18nKey="account.export_data_body" values={{ email: section.values.email }} />
</div>
Expand All @@ -288,6 +289,28 @@ const Account = ({ panelClassName, panelHeaderClassName, canUpdateEmail = true }
}),
]
: []),
...(canDeleteAccount
? [
formSection({
label: t('account.delete_account_title'),
content: () => (
<div className={styles.textWithButtonContainer}>
<div>{t('account.delete_account_body')}</div>
<div>
<Button
label={t('account.delete_account_title')}
type="button"
variant="danger"
onClick={() => {
navigate(addQueryParam(location, 'u', 'delete-account'));
}}
/>
</div>
</div>
),
}),
]
: []),
]}
</Form>
{canExportAccountData && (
Expand Down
12 changes: 12 additions & 0 deletions src/components/Button/Button.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,18 @@ $large-button-height: 40px;
}
}

&.danger {
color: theme.$btn-danger-color;
background-color: theme.$btn-danger-bg;
outline: theme.$btn-danger-outline;
}

&.delete {
color: theme.$btn-delete-color;
background-color: theme.$btn-delete-bg;
outline: none;
}

&.fullWidth {
justify-content: center;
width: 100%;
Expand Down
2 changes: 1 addition & 1 deletion src/components/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import Spinner from '#components/Spinner/Spinner';

type Color = 'default' | 'primary';

type Variant = 'contained' | 'outlined' | 'text';
type Variant = 'contained' | 'outlined' | 'text' | 'danger' | 'delete';

type Props = {
children?: React.ReactNode;
Expand Down
70 changes: 70 additions & 0 deletions src/components/DeleteAccountModal/DeleteAccountModal.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
@use 'src/styles/variables';

.formContainer {
display: flex;
flex-direction: column;
gap: 40px;
align-items: center;
padding: 30px;
}

.formContainerSmall {
gap: 20px;
}

.innerContainer {
display: flex;
flex-direction: column;
gap: 16px;
align-items: center;
}

.passwordButtonsContainer {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
width: 100%;
}

.buttonsContainer {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
width: 100%;
gap: 16px;
}

.heading {
width: 50%;
margin-bottom: 20px;
color: variables.$dark-onbackground-main;
font-weight: 400;
font-size: 20px;
line-height: 28px;
text-align: center;
}

.paragraph {
width: 100%;
margin: 0;
padding: 0;
color: variables.$dark-onbackground-main;
font-size: 14px;
line-height: 20px;
text-align: left;
}

.button {
margin: auto;
}

.warningBox {
display: flex;
flex-direction: column;
padding: 8px 16px 8px 16px;
background-color: variables.$dark-warning-surface;
border-radius: 4px;
gap: 16px;
}
101 changes: 101 additions & 0 deletions src/components/DeleteAccountModal/DeleteAccountModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { useTranslation } from 'react-i18next';
import { type SchemaOf, object, string } from 'yup';
import { useNavigate, useLocation } from 'react-router';
import { useState } from 'react';
import { useMutation } from 'react-query';

import PasswordField from '../PasswordField/PasswordField';
import Button from '../Button/Button';

import styles from './DeleteAccountModal.module.scss';

import type { DeleteAccountFormData } from '#types/account';
import useForm from '#src/hooks/useForm';
import { addQueryParam, removeQueryParam } from '#src/utils/location';
import { deleteAccountData, logout } from '#src/stores/AccountController';

const DeleteAccountModal = () => {
const { t } = useTranslation('user');

const [enteredPassword, setEnteredPassword] = useState<string>('');

const deleteAccount = useMutation(deleteAccountData, {
onSuccess: async () => {
await logout();
navigate('/');
},
onError: () => {
setEnteredPassword('');
handleCancel();
},
});

const navigate = useNavigate();
const location = useLocation();

const validationSchema: SchemaOf<DeleteAccountFormData> = object().shape({
password: string().required(t('login.field_required')),
});
const initialValues: DeleteAccountFormData = { password: '' };
const { handleSubmit, handleChange, values, errors } = useForm(
initialValues,
() => {
setEnteredPassword(values.password);
navigate(addQueryParam(location, 'confirmation', 'true'));
},
validationSchema,
);

const handleCancel = () => {
const removedU = removeQueryParam(location, 'u');
navigate(removedU.split('&confirmation')[0]);
};

return enteredPassword ? (
<div className={styles.formContainer}>
<h2 className={styles.heading}>{t('account.delete_account_title')}</h2>
<div className={styles.innerContainer}>
<p className={styles.paragraph}>{t('account.delete_account_modal_text_1')}</p>
<p className={styles.paragraph}>{t('account.delete_account_modal_text_2')}</p>
<div className={styles.warningBox}>
<p className={styles.paragraph}>{t('account.delete_account_modal_warning_1')}</p>
<p className={styles.paragraph}>{t('account.delete_account_modal_warning_2')}</p>
</div>
<p className={styles.paragraph}>
{t('account.delete_account_modal_text_3')} <br />
{t('account.delete_account_modal_text_4')}
</p>
</div>
<div className={styles.buttonsContainer}>
<Button disabled={deleteAccount.isLoading} variant="text" label={t('account.cancel')} onClick={handleCancel} />
<Button
disabled={deleteAccount.isLoading}
variant="delete"
label={t('account.delete_account_title')}
onClick={() => {
deleteAccount.mutate(enteredPassword);
}}
/>
</div>
</div>
) : (
<form onSubmit={handleSubmit} className={`${styles.formContainer} ${styles.formContainerSmall}`}>
<h2 className={styles.heading}>{t('account.delete_account_modal_title')}</h2>
<PasswordField
value={values.password}
onChange={handleChange}
label={t('account.password')}
placeholder={t('account.delete_account_modal_placeholder')}
error={!!errors.password || !!errors.form}
name="password"
showHelperText={false}
/>
<div className={styles.passwordButtonsContainer}>
<Button type="submit" className={styles.button} color="primary" fullWidth label={t('account.continue')} />
<Button onClick={handleCancel} className={styles.button} variant="text" fullWidth label={t('account.cancel')} />
</div>
</form>
);
};

export default DeleteAccountModal;
4 changes: 4 additions & 0 deletions src/components/Dialog/Dialog.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@
display: none;
}
}

.largeDialog {
max-width: 768px;
}
5 changes: 3 additions & 2 deletions src/components/Dialog/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ import ModalCloseButton from '#components/ModalCloseButton/ModalCloseButton';
type Props = {
open: boolean;
onClose: () => void;
size?: 'small' | 'large';
children: React.ReactNode;
};

const Dialog: React.FC<Props> = ({ open, onClose, children }: Props) => {
const Dialog: React.FC<Props> = ({ open, onClose, size = 'small', children }: Props) => {
return (
<Modal open={open} onClose={onClose} AnimationComponent={Slide}>
<div className={styles.dialog}>
<div className={`${styles.dialog}${size === 'large' ? ` ${styles.largeDialog}` : ''}`}>
<ModalCloseButton onClick={onClose} />
{children}
</div>
Expand Down
14 changes: 11 additions & 3 deletions src/containers/AccountModal/AccountModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@ import PaymentFailed from '#components/PaymentFailed/PaymentFailed';
import Dialog from '#components/Dialog/Dialog';
import { addQueryParam, removeQueryParam } from '#src/utils/location';
import WaitingForPayment from '#src/components/WaitingForPayment/WaitingForPayment';
import DeleteAccountModal from '#src/components/DeleteAccountModal/DeleteAccountModal';

const PUBLIC_VIEWS = ['login', 'create-account', 'forgot-password', 'reset-password', 'send-confirmation', 'edit-password'];

const AccountModal = () => {
const navigate = useNavigate();
const location = useLocation();
const viewParam = useQueryParam('u');
const isConfirmation = useQueryParam('confirmation');
const [view, setView] = useState(viewParam);
const message = useQueryParam('message');
const { loading, auth } = useAccountStore(({ loading, auth }) => ({ loading, auth }), shallow);
Expand All @@ -51,7 +53,8 @@ const AccountModal = () => {
}, [viewParam, navigate, location, loading, auth, isPublicView]);

const closeHandler = () => {
navigate(removeQueryParam(location, 'u'));
const removedU = removeQueryParam(location, 'u');
navigate(isConfirmation ? removedU.split('&confirmation')[0] : removedU);
};

const renderForm = () => {
Expand Down Expand Up @@ -83,6 +86,8 @@ const AccountModal = () => {
return <ResetPassword type="reset" />;
case 'forgot-password':
return <ResetPassword type="forgot" />;
case 'delete-account':
return <DeleteAccountModal />;
case 'send-confirmation':
return <ResetPassword type="confirmation" />;
case 'edit-password':
Expand All @@ -96,9 +101,12 @@ const AccountModal = () => {
}
};

const shouldHideBanner = ['delete-account'].includes(view ?? '');
const dialogSize = isConfirmation ? 'large' : 'small';

return (
<Dialog open={!!viewParam} onClose={closeHandler}>
<div className={styles.banner}>{banner ? <img src={banner} alt="" /> : null}</div>
<Dialog size={dialogSize} open={!!viewParam} onClose={closeHandler}>
{!shouldHideBanner && banner && <div className={styles.banner}>{banner ? <img src={banner} alt="" /> : null}</div>}
{renderForm()}
</Dialog>
);
Expand Down
15 changes: 14 additions & 1 deletion src/i18n/locales/en_US/user.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"account": {
"about_you": "About you",
"continue": "Continue",
"cancel": "Cancel",
"confirm_password": "Confirm password",
"edit_account": "Edit account",
Expand Down Expand Up @@ -29,7 +30,19 @@
"export_data_title": "Export account data",
"export_data_body": "Export all of your personal account data to <strong>{{email}}</strong> as an attachment.",
"export_data_success": "Please check your email.",
"export_data_error": "An error occurred while exporting your account data."
"export_data_error": "An error occurred while exporting your account data.",
"delete_account_title": "Delete account",
"delete_account_body": "Permanently delete your account and all of your content.",
"delete_account_modal_title": "Enter password to continue",
"delete_account_modal_placeholder": "Enter your password",
"delete_account_modal_text_1": "Please note that if you choose to proceed with this process, all of your personal data will be erased from our system. This includes any personal information you provided during the registration process, as well as any location and device information that we gathered during the authentication process. Additionally, all other personal information stored in various parts of our database will be deleted as well. However, please be aware that due to some legislations we are required to keep certain personal data as part of transaction and subscription records.",
"delete_account_modal_text_2": "Once the erasure is complete, all of your active accesses will be revoked, and any active login sessions, including this one, will be terminated. Your account information will no longer be accessible to our staff in any capacity.",
"delete_account_modal_text_warning_1": "All of your active subscriptions purchased through a web browser will be automatically canceled, in addition to the data erasure.",
"delete_account_modal_text_warning_2": "However, all active subscriptions that were purchased through a web browser will be automatically canceled upon erasure of personal data. However, if you purchased a subscription through iOS, Android, Roku, or Amazon apps, please be aware that it will not be canceled automatically. In this case, you will need to follow the usual subscription cancelation steps for each respective platform to manually cancel the subscription.",
"delete_account_modal_text_3": "Please be aware that this process cannot be undone, so proceed with caution.",
"delete_account_modal_text_4": "If you have any further questions or concerns, please do not hesitate to contact our support team.",
"delete_account_modal_warning_1": "All of your active subscriptions purchased through a web browser will be automatically canceled, in addition to the data erasure.",
"delete_account_modal_warning_2": "However, all active subscriptions that were purchased through a web browser will be automatically canceled upon erasure of personal data. However, if you purchased a subscription through iOS, Android, Roku, or Amazon apps, please be aware that it will not be canceled automatically. In this case, you will need to follow the usual subscription cancelation steps for each respective platform to manually cancel the subscription."
},
"favorites": {
"clear": "Clear favorites",
Expand Down
4 changes: 4 additions & 0 deletions src/services/cleeng.account.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,11 @@ const handleErrors = (errors: ApiResponse['errors']) => {
export const updatePersonalShelves: UpdatePersonalShelves = async (payload, sandbox, jwt) => {
return await updateCustomer(payload, sandbox, jwt);
};

export const exportAccountData = () => null;

export const deleteAccount = () => null;

export const canUpdateEmail = true;

export const canSupportEmptyFullName = true;
Expand All @@ -230,3 +233,4 @@ export const canChangePasswordWithOldPassword = false;
export const subscribeToNotifications = async () => true;
export const canRenewSubscription = true;
export const canExportAccountData = false;
export const canDeleteAccount = false;
Loading

0 comments on commit b6eda04

Please sign in to comment.