1 import type { ChangeEvent, FormEvent } from 'react';
2 import { useState } from 'react';
4 import { c } from 'ttag';
6 import { useGetOrganization } from '@proton/account/organization/hooks';
7 import { useUser } from '@proton/account/user/hooks';
8 import { Button, Href } from '@proton/atoms';
9 import Alert from '@proton/components/components/alert/Alert';
10 import ErrorButton from '@proton/components/components/button/ErrorButton';
11 import Form from '@proton/components/components/form/Form';
12 import Checkbox from '@proton/components/components/input/Checkbox';
13 import type { ModalProps } from '@proton/components/components/modalTwo/Modal';
14 import ModalTwo from '@proton/components/components/modalTwo/Modal';
15 import ModalTwoContent from '@proton/components/components/modalTwo/ModalContent';
16 import ModalTwoFooter from '@proton/components/components/modalTwo/ModalFooter';
17 import ModalTwoHeader from '@proton/components/components/modalTwo/ModalHeader';
18 import useModalState from '@proton/components/components/modalTwo/useModalState';
19 import Option from '@proton/components/components/option/Option';
20 import SelectTwo from '@proton/components/components/selectTwo/SelectTwo';
21 import InputFieldTwo from '@proton/components/components/v2/field/InputField';
22 import TextAreaTwo from '@proton/components/components/v2/input/TextArea';
23 import useFormErrors from '@proton/components/components/v2/useFormErrors';
24 import AuthModal from '@proton/components/containers/password/AuthModal';
25 import useApi from '@proton/components/hooks/useApi';
26 import useAuthentication from '@proton/components/hooks/useAuthentication';
27 import useConfig from '@proton/components/hooks/useConfig';
28 import useEventManager from '@proton/components/hooks/useEventManager';
29 import useNotifications from '@proton/components/hooks/useNotifications';
30 import { useLoading } from '@proton/hooks';
31 import { leaveOrganisation } from '@proton/shared/lib/api/organization';
32 import { canDelete, deleteUser, unlockPasswordChanges } from '@proton/shared/lib/api/user';
33 import { handleLogout } from '@proton/shared/lib/authentication/logout';
34 import { ACCOUNT_DELETION_REASONS, BRAND_NAME } from '@proton/shared/lib/constants';
35 import { minLengthValidator, requiredValidator } from '@proton/shared/lib/helpers/formValidators';
36 import { wait } from '@proton/shared/lib/helpers/promise';
37 import { getKnowledgeBaseUrl } from '@proton/shared/lib/helpers/url';
38 import { getOrganizationDenomination } from '@proton/shared/lib/organization/helper';
39 import isTruthy from '@proton/utils/isTruthy';
40 import noop from '@proton/utils/noop';
42 const { DIFFERENT_ACCOUNT, TOO_EXPENSIVE, MISSING_FEATURE, USE_OTHER_SERVICE, OTHER } = ACCOUNT_DELETION_REASONS;
44 interface Props extends Omit<ModalProps<'form'>, 'as'> {
45 onSuccess?: () => Promise<void>;
49 const SupportParagraph = () => {
51 /* translator: Full sentence: "This message won't reach the support team, if you have an issue with our service or need further action from our side please open a support ticket." */
54 <Href key="link-to-support" href="https://proton.me/support/contact-product">{c('Info')
55 .t`please open a support ticket`}</Href>
59 <p className="text-sm color-weak mt-0">
60 {/* translator: Full sentence: "This message won't reach the support team, if you have an issue with our service or need further action from our side please open a support ticket." */}
62 .jt`This message won't reach the support team, if you have an issue with our service or need further action from our side ${supportLink}.`}
67 const DeleteAccountModal = (props: Props) => {
68 const [authModalProps, setAuthModalOpen, renderAuthModal] = useModalState();
69 const { createNotification } = useNotifications();
71 const defaultOnSuccess = async () => {
72 createNotification({ text: c('Success').t`Account deleted. Signing out...` });
74 // Add an artificial delay to show the notification.
79 onSuccess = defaultOnSuccess,
86 const eventManager = useEventManager();
88 const authentication = useAuthentication();
89 const [{ isAdmin }] = useUser();
90 const getOrganization = useGetOrganization();
91 const [loading, withLoading] = useLoading();
92 const [model, setModel] = useState({
97 const { validator, onFormSubmit } = useFormErrors();
98 const { APP_NAME } = useConfig();
102 title={c('Option').t`I use a different ${BRAND_NAME} account`}
103 value={DIFFERENT_ACCOUNT}
104 key={DIFFERENT_ACCOUNT}
106 isAdmin && <Option title={c('Option').t`It's too expensive`} value={TOO_EXPENSIVE} key={TOO_EXPENSIVE} />,
108 title={c('Option').t`It's missing a key feature that I need`}
109 value={MISSING_FEATURE}
110 key={MISSING_FEATURE}
113 title={c('Option').t`I found another service that I like better`}
114 value={USE_OTHER_SERVICE}
115 key={USE_OTHER_SERVICE}
117 <Option title={c('Option').t`My reason isn't listed`} value={OTHER} key={OTHER} />,
120 const handleSubmit = async () => {
124 await api(canDelete());
126 const organization = await getOrganization();
127 // If a user is part of a family plan or duo plan we first need to leave the organization before deleting the account.
128 // Refreshing the event manager is necessary to update the organization state
129 if (getOrganizationDenomination(organization) === 'familyGroup' && !isAdmin) {
130 eventManager.start();
131 await api(leaveOrganisation());
132 await eventManager.call();
138 Reason: model.reason,
139 Feedback: model.feedback,
145 handleLogout({ appName: APP_NAME, authentication, clearDeviceRecoveryData: true, type: 'full' });
146 } catch (error: any) {
147 eventManager.start();
154 {renderAuthModal && (
157 config={unlockPasswordChanges()}
161 withLoading(handleSubmit());
167 onClose={hideHeader ? undefined : onClose}
168 disableCloseOnEscape={disableCloseOnEscape || loading}
173 : (event: FormEvent<HTMLFormElement>) => {
174 if (!onFormSubmit(event.currentTarget)) {
177 setAuthModalOpen(true);
182 {!hideHeader && <ModalTwoHeader title={c('Title').t`Delete account`} />}
184 <Alert className="mb-4" type="warning">
185 <div className="text-bold text-uppercase">
187 .t`Warning: deletion is permanent. This also removes access to all connected services and deletes all of your contacts.`}
190 {c('Info').t`If you wish to combine this account with another one, do NOT delete it.`}
193 <Href href={getKnowledgeBaseUrl('/combine-accounts')}>{c('Link').t`Learn more`}</Href>
198 label={c('Label').t`What is the main reason you are deleting your account?`}
199 placeholder={c('Placeholder').t`Select a reason`}
203 onValue={(value: unknown) => setModel({ ...model, reason: value as string })}
204 error={validator([requiredValidator(model.reason)])}
215 .t`We are sorry to see you go. Please explain why you are leaving to help us improve.`}
216 placeholder={c('Placeholder').t`Feedback`}
217 value={model.feedback}
218 onValue={(value: string) => setModel({ ...model, feedback: value })}
219 error={validator([requiredValidator(model.feedback), minLengthValidator(model.feedback, 10)])}
226 error={validator([!model.check ? requiredValidator(undefined) : ''])}
227 checked={model.check}
228 onChange={({ target }: ChangeEvent<HTMLInputElement>) =>
229 setModel({ ...model, check: target.checked })
233 {c('Label').t`Yes, I want to permanently delete this account and all its data.`}
239 <Button onClick={onClose} disabled={loading}>{c('Action').t`Cancel`}</Button>
240 <ErrorButton loading={loading} type="submit">
241 {c('Action').t`Delete`}
249 export default DeleteAccountModal;