accountClose.tsx 6.0 KB

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