accountClose.tsx 5.8 KB

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