accountSubscriptions.tsx 8.0 KB

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