accountClose.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. import {useEffect, useState} from 'react';
  2. import {css} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import {addErrorMessage, addLoadingMessage} from 'sentry/actionCreators/indicator';
  5. import type {ModalRenderProps} from 'sentry/actionCreators/modal';
  6. import {openModal} from 'sentry/actionCreators/modal';
  7. import {fetchOrganizations} from 'sentry/actionCreators/organizations';
  8. import {Alert} from 'sentry/components/alert';
  9. import {LinkButton} from 'sentry/components/button';
  10. import Checkbox from 'sentry/components/checkbox';
  11. import HookOrDefault from 'sentry/components/hookOrDefault';
  12. import LoadingIndicator from 'sentry/components/loadingIndicator';
  13. import Panel from 'sentry/components/panels/panel';
  14. import PanelAlert from 'sentry/components/panels/panelAlert';
  15. import PanelBody from 'sentry/components/panels/panelBody';
  16. import PanelHeader from 'sentry/components/panels/panelHeader';
  17. import PanelItem from 'sentry/components/panels/panelItem';
  18. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  19. import {t} from 'sentry/locale';
  20. import type {Organization, OrganizationSummary} from 'sentry/types/organization';
  21. import useApi from 'sentry/utils/useApi';
  22. import {ConfirmAccountClose} from 'sentry/views/settings/account/confirmAccountClose';
  23. import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
  24. import TextBlock from 'sentry/views/settings/components/text/textBlock';
  25. const BYE_URL = '/';
  26. const leaveRedirect = () => (window.location.href = BYE_URL);
  27. const Important = styled('div')`
  28. font-weight: ${p => p.theme.fontWeightBold};
  29. font-size: 1.2em;
  30. `;
  31. function GoodbyeModalContent({Header, Body, Footer}: ModalRenderProps) {
  32. return (
  33. <div>
  34. <Header>{t('Closing Account')}</Header>
  35. <Body>
  36. <TextBlock>
  37. {t('Your account has been deactivated and scheduled for removal.')}
  38. </TextBlock>
  39. <TextBlock>
  40. {t('Thanks for using Sentry! We hope to see you again soon!')}
  41. </TextBlock>
  42. </Body>
  43. <Footer>
  44. <LinkButton href={BYE_URL}>{t('Goodbye')}</LinkButton>
  45. </Footer>
  46. </div>
  47. );
  48. }
  49. type OwnedOrg = {
  50. organization: Organization;
  51. singleOwner: boolean;
  52. };
  53. function AccountClose() {
  54. const api = useApi();
  55. const [organizations, setOrganizations] = useState<OwnedOrg[]>([]);
  56. const [orgsToRemove, setOrgsToRemove] = useState<Set<string>>(new Set());
  57. const [isLoading, setIsLoading] = useState(true);
  58. // Load organizations from all regions.
  59. useEffect(() => {
  60. setIsLoading(true);
  61. fetchOrganizations(api, {owner: 1}).then((response: OwnedOrg[]) => {
  62. const singleOwnerOrgs = response
  63. .filter(item => item.singleOwner)
  64. .map(item => item.organization.slug);
  65. setOrgsToRemove(new Set(singleOwnerOrgs));
  66. setOrganizations(response);
  67. setIsLoading(false);
  68. });
  69. }, [api]);
  70. let leaveRedirectTimeout: number | undefined = undefined;
  71. useEffect(() => {
  72. // setup unmount callback
  73. return () => {
  74. window.clearTimeout(leaveRedirectTimeout);
  75. };
  76. }, [leaveRedirectTimeout]);
  77. const handleChange = (
  78. organization: OrganizationSummary,
  79. isSingle: boolean,
  80. event: React.ChangeEvent<HTMLInputElement>
  81. ) => {
  82. const checked = event.target.checked;
  83. // Can't unselect an org where you are the single owner
  84. if (isSingle) {
  85. return;
  86. }
  87. const slugSet = new Set(orgsToRemove);
  88. if (checked) {
  89. slugSet.add(organization.slug);
  90. } else {
  91. slugSet.delete(organization.slug);
  92. }
  93. setOrgsToRemove(slugSet);
  94. };
  95. const handleRemoveAccount = async () => {
  96. addLoadingMessage('Closing account\u2026');
  97. try {
  98. await api.requestPromise('/users/me/', {
  99. method: 'DELETE',
  100. data: {organizations: Array.from(orgsToRemove)},
  101. });
  102. openModal(GoodbyeModalContent, {
  103. onClose: leaveRedirect,
  104. });
  105. // Redirect after 10 seconds
  106. window.clearTimeout(leaveRedirectTimeout);
  107. leaveRedirectTimeout = window.setTimeout(leaveRedirect, 10000);
  108. } catch {
  109. addErrorMessage('Error closing account');
  110. }
  111. };
  112. const HookedCustomConfirmAccountClose = HookOrDefault({
  113. hookName: 'component:confirm-account-close',
  114. defaultComponent: props => <ConfirmAccountClose {...props} />,
  115. });
  116. if (isLoading) {
  117. return <LoadingIndicator />;
  118. }
  119. return (
  120. <div>
  121. <SentryDocumentTitle title={t('Close Account')} />
  122. <SettingsPageHeader title={t('Close Account')} />
  123. <TextBlock>
  124. {t(
  125. 'This will permanently remove all associated data for your user. Any specified organizations will also be deleted.'
  126. )}
  127. </TextBlock>
  128. <Alert type="error" showIcon>
  129. <Important>
  130. {t('Closing your account is permanent and cannot be undone')}!
  131. </Important>
  132. </Alert>
  133. <Panel>
  134. <PanelHeader>{t('Delete the following organizations')}</PanelHeader>
  135. <PanelBody>
  136. <PanelAlert type="warning">
  137. <strong>{t('ORGANIZATIONS WITH CHECKED BOXES WILL BE DELETED!')}</strong>
  138. <br />
  139. {t(
  140. 'Ownership will remain with other organization owners if an organization is not deleted.'
  141. )}
  142. <br />
  143. {t(
  144. "Boxes which can't be unchecked mean that you are the only organization owner and the organization will be deleted."
  145. )}
  146. </PanelAlert>
  147. {organizations?.map(({organization, singleOwner}) => (
  148. <PanelItem key={organization.slug}>
  149. <PanelLabel>
  150. <Checkbox
  151. css={css`
  152. margin-right: 6px;
  153. `}
  154. name="organizations"
  155. checked={orgsToRemove.has(organization.slug)}
  156. disabled={singleOwner}
  157. value={organization.slug}
  158. onChange={evt => handleChange(organization, singleOwner, evt)}
  159. size="sm"
  160. role="checkbox"
  161. />
  162. {organization.slug}
  163. </PanelLabel>
  164. </PanelItem>
  165. ))}
  166. </PanelBody>
  167. </Panel>
  168. <HookedCustomConfirmAccountClose
  169. handleRemoveAccount={handleRemoveAccount}
  170. organizationSlugs={Array.from(orgsToRemove)}
  171. />
  172. </div>
  173. );
  174. }
  175. const PanelLabel = styled('label')`
  176. display: flex;
  177. align-items: center;
  178. `;
  179. export default AccountClose;