trialStartedSidebarItem.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. import {Component} from 'react';
  2. import type {Theme} from '@emotion/react';
  3. import {css, withTheme} from '@emotion/react';
  4. import styled from '@emotion/styled';
  5. import {motion} from 'framer-motion';
  6. import type {Client} from 'sentry/api';
  7. import {Button} from 'sentry/components/button';
  8. import {Hovercard} from 'sentry/components/hovercard';
  9. import {IconBusiness} from 'sentry/icons';
  10. import {t} from 'sentry/locale';
  11. import {space} from 'sentry/styles/space';
  12. import type {Organization} from 'sentry/types/organization';
  13. import testableTransition from 'sentry/utils/testableTransition';
  14. import withApi from 'sentry/utils/withApi';
  15. import TrialRequestedActions from 'getsentry/actions/trialRequestedActions';
  16. import SubscriptionStore from 'getsentry/stores/subscriptionStore';
  17. import TrialRequestedStore from 'getsentry/stores/trialRequestedStore';
  18. import type {Subscription} from 'getsentry/types';
  19. import {hasJustStartedPlanTrial} from 'getsentry/utils/billing';
  20. import TrialBadge from 'getsentry/views/subscriptionPage/trial/badge';
  21. type Props = {
  22. api: Client;
  23. children: React.ReactNode;
  24. organization: Organization;
  25. subscription: Subscription;
  26. theme: Theme;
  27. };
  28. type State = {
  29. animationComplete: boolean;
  30. trialRequested: boolean;
  31. };
  32. class TrialStartedSidebarItem extends Component<Props, State> {
  33. state: State = {
  34. animationComplete: !!hasJustStartedPlanTrial(this.props.subscription),
  35. trialRequested: TrialRequestedStore.getTrialRequstedState(),
  36. };
  37. componentWillUnmount() {
  38. this.unsubscribe();
  39. }
  40. unsubscribe = TrialRequestedStore.listen(
  41. () => this.setState({trialRequested: TrialRequestedStore.getTrialRequstedState()}),
  42. undefined
  43. );
  44. dismissNotification = () => {
  45. SubscriptionStore.clearStartedTrial(this.props.organization.slug);
  46. TrialRequestedActions.clearNotification();
  47. };
  48. renderTrialStartedHovercardBody() {
  49. return (
  50. <HovercardBody>
  51. <HovercardHeader>
  52. <div>{t('Trial Started')}</div>
  53. <TrialBadge
  54. subscription={this.props.subscription}
  55. organization={this.props.organization}
  56. />
  57. </HovercardHeader>
  58. <p>{t('Check out these great new features')}</p>
  59. <Bullets>
  60. <IconBusiness gradient />
  61. {t('Application Insights')}
  62. <IconBusiness gradient />
  63. {t('Dashboards')}
  64. <IconBusiness gradient />
  65. {t('Advanced Discover Queries')}
  66. <IconBusiness gradient />
  67. {t('Additional Integrations')}
  68. </Bullets>
  69. <Button onClick={this.dismissNotification} size="xs">
  70. {t('Awesome, got it!')}
  71. </Button>
  72. </HovercardBody>
  73. );
  74. }
  75. renderTrialRequestedHovercardBody() {
  76. return (
  77. <HovercardBody>
  78. <HovercardHeader>{t('Trial Requested')}</HovercardHeader>
  79. <p>
  80. {t(
  81. 'We have notified your organization owner that you want to start a Sentry trial.'
  82. )}
  83. </p>
  84. <Button onClick={this.dismissNotification} size="xs">
  85. {t('Awesome, got it!')}
  86. </Button>
  87. </HovercardBody>
  88. );
  89. }
  90. renderWithHovercard(hovercardBody: React.ReactNode) {
  91. return (
  92. <StyledHovercard forceVisible position="right" body={hovercardBody}>
  93. {this.props.children}
  94. </StyledHovercard>
  95. );
  96. }
  97. get trialRequestedOrStarted() {
  98. const {trialRequested} = this.state;
  99. return hasJustStartedPlanTrial(this.props.subscription) || trialRequested;
  100. }
  101. render() {
  102. const {animationComplete, trialRequested} = this.state;
  103. const {theme} = this.props;
  104. const animate =
  105. animationComplete && !this.trialRequestedOrStarted
  106. ? 'dismissed'
  107. : this.trialRequestedOrStarted
  108. ? 'started'
  109. : 'initial';
  110. let children = this.props.children;
  111. if (animationComplete) {
  112. if (hasJustStartedPlanTrial(this.props.subscription)) {
  113. children = this.renderWithHovercard(this.renderTrialStartedHovercardBody());
  114. } else if (trialRequested) {
  115. children = this.renderWithHovercard(this.renderTrialRequestedHovercardBody());
  116. }
  117. }
  118. return (
  119. <BoxShadowHider>
  120. <Wrapper
  121. initial={animate}
  122. onAnimationComplete={() =>
  123. setTimeout(() => this.setState({animationComplete: true}), 500)
  124. }
  125. animate={animate}
  126. variants={{
  127. initial: {
  128. backgroundImage: `linear-gradient(-45deg, ${theme.purple400} 0%, transparent 0%)`,
  129. },
  130. started: {
  131. backgroundImage: `linear-gradient(-45deg, ${theme.purple400} 100%, transparent 0%)`,
  132. // We flip the gradient direction so that on dismiss we can animate in the
  133. // opposite direction.
  134. transitionEnd: {
  135. backgroundImage: `linear-gradient(45deg, ${theme.purple400} 100%, transparent 0%)`,
  136. },
  137. transition: testableTransition({
  138. duration: 0.35,
  139. delay: 1,
  140. }),
  141. },
  142. }}
  143. >
  144. {children}
  145. </Wrapper>
  146. </BoxShadowHider>
  147. );
  148. }
  149. }
  150. const startedStyle = (theme: Theme) => css`
  151. transition: box-shadow 200ms;
  152. color: ${theme.white};
  153. &:hover a {
  154. color: ${theme.white};
  155. }
  156. &:hover {
  157. box-shadow: 0 0 8px ${theme.purple400};
  158. }
  159. `;
  160. const Wrapper = styled(motion.div)`
  161. margin: 0 -20px 0 -5px;
  162. padding: 0 20px 0 5px;
  163. border-radius: 4px 0 0 4px;
  164. ${p => p.animate === 'started' && startedStyle(p.theme)}
  165. /* This is needed to fix positioning of the hovercard, since it wraps a
  166. * inline span, the span has no size and the position is incorrectly
  167. * computed, causing the hovercard to appear far from the nav item */
  168. span[aria-describedby] {
  169. display: block;
  170. }
  171. `;
  172. const BoxShadowHider = styled('div')`
  173. margin: -20px;
  174. padding: 20px;
  175. overflow: hidden;
  176. `;
  177. // We specifically set the z-index lower than the modal here, since it will be
  178. // common to start a trial with the upsell modal open.
  179. const StyledHovercard = styled(Hovercard)`
  180. width: 310px;
  181. z-index: ${p => p.theme.zIndex.modal - 1};
  182. margin-left: 30px;
  183. `;
  184. const HovercardBody = styled('div')`
  185. h1 {
  186. font-size: ${p => p.theme.fontSizeLarge};
  187. margin-bottom: ${space(1.5)};
  188. }
  189. p {
  190. font-size: ${p => p.theme.fontSizeMedium};
  191. }
  192. `;
  193. const Bullets = styled('div')`
  194. display: grid;
  195. grid-template-columns: max-content 1fr;
  196. grid-auto-rows: max-content;
  197. gap: ${space(1)};
  198. align-items: center;
  199. font-size: ${p => p.theme.fontSizeMedium};
  200. margin-bottom: ${space(2)};
  201. `;
  202. const HovercardHeader = styled('h1')`
  203. display: flex;
  204. gap: ${space(1)};
  205. align-items: center;
  206. `;
  207. export default withApi(withTheme(TrialStartedSidebarItem));