accountClose.tsx 6.2 KB

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