accountSubscriptions.tsx 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import groupBy from 'lodash/groupBy';
  4. import moment from 'moment';
  5. import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
  6. import DateTime from 'sentry/components/dateTime';
  7. import {Panel, PanelBody, PanelHeader, PanelItem} from 'sentry/components/panels';
  8. import Switch from 'sentry/components/switchButton';
  9. import {IconToggle} from 'sentry/icons';
  10. import {t, tct} from 'sentry/locale';
  11. import space from 'sentry/styles/space';
  12. import AsyncView from 'sentry/views/asyncView';
  13. import EmptyMessage from 'sentry/views/settings/components/emptyMessage';
  14. import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
  15. import TextBlock from 'sentry/views/settings/components/text/textBlock';
  16. const ENDPOINT = '/users/me/subscriptions/';
  17. type Subscription = {
  18. email: string;
  19. listDescription: string;
  20. listId: number;
  21. listName: string;
  22. subscribed: boolean;
  23. subscribedDate: string | null;
  24. unsubscribedDate: string | null;
  25. };
  26. type State = AsyncView['state'] & {
  27. subscriptions: Subscription[];
  28. };
  29. class AccountSubscriptions extends AsyncView<AsyncView['props'], State> {
  30. getEndpoints(): ReturnType<AsyncView['getEndpoints']> {
  31. return [['subscriptions', ENDPOINT]];
  32. }
  33. getTitle() {
  34. return 'Subscriptions';
  35. }
  36. handleToggle = (subscription: Subscription, index: number, _e: React.MouseEvent) => {
  37. const subscribed = !subscription.subscribed;
  38. const oldSubscriptions = this.state.subscriptions;
  39. this.setState(state => {
  40. const newSubscriptions = state.subscriptions.slice();
  41. newSubscriptions[index] = {
  42. ...subscription,
  43. subscribed,
  44. subscribedDate: new Date().toString(),
  45. };
  46. return {
  47. ...state,
  48. subscriptions: newSubscriptions,
  49. };
  50. });
  51. this.api.request(ENDPOINT, {
  52. method: 'PUT',
  53. data: {
  54. listId: subscription.listId,
  55. subscribed,
  56. },
  57. success: () => {
  58. addSuccessMessage(
  59. `${subscribed ? 'Subscribed' : 'Unsubscribed'} to ${subscription.listName}`
  60. );
  61. },
  62. error: () => {
  63. addErrorMessage(
  64. `Unable to ${subscribed ? '' : 'un'}subscribe to ${subscription.listName}`
  65. );
  66. this.setState({subscriptions: oldSubscriptions});
  67. },
  68. });
  69. };
  70. renderBody() {
  71. const subGroups = Object.entries(groupBy(this.state.subscriptions, sub => sub.email));
  72. return (
  73. <div>
  74. <SettingsPageHeader title="Subscriptions" />
  75. <TextBlock>
  76. {t(`Sentry is committed to respecting your inbox. Our goal is to
  77. provide useful content and resources that make fixing errors less
  78. painful. Enjoyable even.`)}
  79. </TextBlock>
  80. <TextBlock>
  81. {t(`As part of our compliance with the EU’s General Data Protection
  82. Regulation (GDPR), starting on 25 May 2018, we’ll only email you
  83. according to the marketing categories to which you’ve explicitly
  84. opted-in.`)}
  85. </TextBlock>
  86. <Panel>
  87. {this.state.subscriptions.length ? (
  88. <div>
  89. <PanelHeader>{t('Subscription')}</PanelHeader>
  90. <PanelBody>
  91. {subGroups.map(([email, subscriptions]) => (
  92. <Fragment key={email}>
  93. {subGroups.length > 1 && (
  94. <Heading>
  95. <IconToggle /> {t('Subscriptions for %s', email)}
  96. </Heading>
  97. )}
  98. {subscriptions.map((subscription, index) => (
  99. <PanelItem center key={subscription.listId}>
  100. <SubscriptionDetails>
  101. <SubscriptionName>{subscription.listName}</SubscriptionName>
  102. {subscription.listDescription && (
  103. <Description>{subscription.listDescription}</Description>
  104. )}
  105. {subscription.subscribed ? (
  106. <SubscribedDescription>
  107. <div>
  108. {tct('[email] on [date]', {
  109. email: subscription.email,
  110. date: (
  111. <DateTime
  112. date={moment(subscription.subscribedDate!)}
  113. />
  114. ),
  115. })}
  116. </div>
  117. </SubscribedDescription>
  118. ) : (
  119. <SubscribedDescription>
  120. {t('Not currently subscribed')}
  121. </SubscribedDescription>
  122. )}
  123. </SubscriptionDetails>
  124. <div>
  125. <Switch
  126. isActive={subscription.subscribed}
  127. size="lg"
  128. toggle={this.handleToggle.bind(this, subscription, index)}
  129. />
  130. </div>
  131. </PanelItem>
  132. ))}
  133. </Fragment>
  134. ))}
  135. </PanelBody>
  136. </div>
  137. ) : (
  138. <EmptyMessage>{t("There's no subscription backend present.")}</EmptyMessage>
  139. )}
  140. </Panel>
  141. <TextBlock>
  142. {t(`We’re applying GDPR consent and privacy policies to all Sentry
  143. contacts, regardless of location. You’ll be able to manage your
  144. subscriptions here and from an Unsubscribe link in the footer of
  145. all marketing emails.`)}
  146. </TextBlock>
  147. <TextBlock>
  148. {tct(
  149. 'Please contact [email:learn@sentry.io] with any questions or suggestions.',
  150. {email: <a href="mailto:learn@sentry.io" />}
  151. )}
  152. </TextBlock>
  153. </div>
  154. );
  155. }
  156. }
  157. const Heading = styled(PanelItem)`
  158. display: grid;
  159. grid-template-columns: max-content 1fr;
  160. gap: ${space(1)};
  161. align-items: center;
  162. font-size: ${p => p.theme.fontSizeMedium};
  163. padding: ${space(1.5)} ${space(2)};
  164. background: ${p => p.theme.backgroundSecondary};
  165. color: ${p => p.theme.subText};
  166. `;
  167. const SubscriptionDetails = styled('div')`
  168. width: 50%;
  169. padding-right: ${space(2)};
  170. `;
  171. const SubscriptionName = styled('div')`
  172. font-size: ${p => p.theme.fontSizeMedium};
  173. `;
  174. const Description = styled('div')`
  175. font-size: ${p => p.theme.fontSizeSmall};
  176. color: ${p => p.theme.subText};
  177. margin-top: ${space(0.5)};
  178. `;
  179. const SubscribedDescription = styled(Description)`
  180. color: ${p => p.theme.subText};
  181. `;
  182. export default AccountSubscriptions;