accountClose.tsx 5.8 KB

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