accountClose.tsx 5.5 KB

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