partnerPlanEndingModal.tsx 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. import {Fragment} from 'react';
  2. import {css} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import partnerMigrationHero from 'getsentry-images/partnership/plan-ending.svg';
  5. import moment from 'moment-timezone';
  6. import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
  7. import type {ModalRenderProps} from 'sentry/actionCreators/modal';
  8. import {Client} from 'sentry/api';
  9. import {Button, LinkButton} from 'sentry/components/button';
  10. import {Tag} from 'sentry/components/core/badge/tag';
  11. import {IconBusiness} from 'sentry/icons';
  12. import {IconClock} from 'sentry/icons/iconClock';
  13. import {t, tct, tn} from 'sentry/locale';
  14. import {space} from 'sentry/styles/space';
  15. import type {Organization} from 'sentry/types/organization';
  16. import withSubscription from 'getsentry/components/withSubscription';
  17. import type {Subscription} from 'getsentry/types';
  18. import {getContractDaysLeft, isTeamPlanFamily} from 'getsentry/utils/billing';
  19. import trackGetsentryAnalytics from 'getsentry/utils/trackGetsentryAnalytics';
  20. type Props = Pick<ModalRenderProps, 'closeModal'> & {
  21. organization: Organization;
  22. subscription: Subscription;
  23. };
  24. function DeveloperItem(text: string, index: number) {
  25. return (
  26. <Fragment key={index}>
  27. <div>-</div>
  28. {text}
  29. </Fragment>
  30. );
  31. }
  32. function UpgradeItem(text: string, index: number) {
  33. return (
  34. <Fragment key={index}>
  35. <IconBusiness size="sm" />
  36. {text}
  37. </Fragment>
  38. );
  39. }
  40. function PartnerPlanEndingModal({organization, subscription, closeModal}: Props) {
  41. const daysLeft = getContractDaysLeft(subscription);
  42. const partnerName = subscription.partner?.partnership.displayName;
  43. const api = new Client();
  44. const endpoint = `/organizations/${organization.slug}/partner-migration-request/?referrer=partner_plan_ending_modal`;
  45. const handleRequest = () => {
  46. api.request(endpoint, {
  47. method: 'POST',
  48. success: () => {
  49. addSuccessMessage(t('Request sent.'));
  50. closeModal();
  51. },
  52. error: () => {
  53. addErrorMessage(t('Could not send request'));
  54. },
  55. });
  56. };
  57. if (daysLeft < 0) {
  58. closeModal();
  59. return null;
  60. }
  61. const isTeam = isTeamPlanFamily(subscription.planDetails);
  62. const hasBillingAccess = organization.access?.includes('org:billing');
  63. const endDate = moment(subscription.contractPeriodEnd).add(1, 'days').format('ll');
  64. const lastDay = daysLeft === 0;
  65. const returnPlan = isTeam ? t('Team') : t('Business');
  66. const leftColumnItems = [
  67. t('One user'),
  68. t('Error monitoring and tracing'),
  69. t('Email only notifications'),
  70. ];
  71. const rightColumnItems = isTeam
  72. ? [
  73. t('Unlimited users'),
  74. t('Event volume controls'),
  75. t('SSO via Google and Github'),
  76. t('Third party integrations'),
  77. t('Extended data retention'),
  78. t('Custom alerts'),
  79. t('And more…'),
  80. ]
  81. : [
  82. t('Unlimited users'),
  83. t('Application Insights'),
  84. t('Advanced event volume controls'),
  85. t('Custom dashboards'),
  86. t('SAML2 & SCIM'),
  87. t('Third party integrations'),
  88. t('Extended data retention'),
  89. t('And more…'),
  90. ];
  91. return (
  92. <div data-test-id="partner-plan-ending-modal">
  93. <ImageHeader />
  94. <div>
  95. <PartnerPlanHeading>
  96. <Tag icon={<IconClock />} type={'promotion'}>
  97. {tn('%s day left', '%s days left', daysLeft)}
  98. </Tag>
  99. <h2 data-test-id="partner-plan-ending-header">
  100. {tct(`Your promotional plan with [partnerName] ends soon`, {
  101. partnerName,
  102. })}
  103. </h2>
  104. <p data-test-id="partner-plan-ending-body">
  105. {tct(
  106. `
  107. Your Sentry promotional plan with [partnerName] ends
  108. [date]. To keep full functionality, upgrade to stay on your [planName] plan.
  109. `,
  110. {
  111. partnerName,
  112. date: lastDay
  113. ? 'today'
  114. : `on ${moment(subscription.contractPeriodEnd).format('ll')}`,
  115. planName: returnPlan,
  116. }
  117. )}
  118. </p>
  119. </PartnerPlanHeading>
  120. <PathWrapper>
  121. <PathContainer>
  122. <SubHeading>{tct(`New Plan on [endDate]`, {endDate})}</SubHeading>
  123. <PathHeading>{t('Developer')}</PathHeading>
  124. <p>{t('For solo devs working on small projects')}</p>
  125. <Bullets>{leftColumnItems.map(DeveloperItem)}</Bullets>
  126. </PathContainer>
  127. <PathContainer>
  128. <SubHeading>{t('Recommended Plan')}</SubHeading>
  129. <PathHeading>{tct(`[returnPlan]`, {returnPlan})}</PathHeading>
  130. <p>{t('For multiple teams that operate at scale')}</p>
  131. <Bullets data-test-id="partner-plan-ending-bullet">
  132. {rightColumnItems.map(UpgradeItem)}
  133. </Bullets>
  134. </PathContainer>
  135. </PathWrapper>
  136. <div style={{display: 'block'}}>
  137. <StyledButtonBar>
  138. <Button data-test-id="maybe-later" priority={'default'} onClick={closeModal}>
  139. {t('Remind Me Later')}
  140. </Button>
  141. {hasBillingAccess ? (
  142. <LinkButton
  143. size="md"
  144. to={`/settings/${organization.slug}/billing/checkout/?referrer=partner_plan_ending_modal`}
  145. aria-label="Upgrade Now"
  146. priority="primary"
  147. onClick={() =>
  148. trackGetsentryAnalytics('partner_billing_migration.modal.clicked_cta', {
  149. subscription,
  150. organization,
  151. daysLeft,
  152. partner: subscription.partner?.partnership.id,
  153. })
  154. }
  155. >
  156. {t('Upgrade Now')}
  157. </LinkButton>
  158. ) : (
  159. <Button
  160. size="md"
  161. aria-label="Request to Upgrade"
  162. priority="primary"
  163. onClick={handleRequest}
  164. >
  165. {t('Request to Upgrade')}
  166. </Button>
  167. )}
  168. </StyledButtonBar>
  169. </div>
  170. </div>
  171. </div>
  172. );
  173. }
  174. const PartnerPlanHeading = styled('div')`
  175. padding: ${space(3)} 0;
  176. p {
  177. font-size: ${p => p.theme.fontSizeLarge};
  178. margin: 0;
  179. }
  180. h2 {
  181. font-size: 1.5em;
  182. }
  183. `;
  184. const PathWrapper = styled('div')`
  185. display: flex;
  186. justify-content: space-between;
  187. `;
  188. const PathContainer = styled('div')`
  189. padding: ${space(3)};
  190. grid-auto-rows: max-content;
  191. border: 1px solid ${p => p.theme.gray300};
  192. margin-left: auto;
  193. margin-right: auto;
  194. border-radius: 5px;
  195. width: 250px;
  196. &:first-of-type {
  197. border: 1px solid ${p => p.theme.gray100};
  198. }
  199. `;
  200. const StyledButtonBar = styled('div')`
  201. margin-top: ${space(2)};
  202. display: flex;
  203. flex-direction: row;
  204. column-gap: 20px;
  205. text-align: right;
  206. justify-content: right;
  207. `;
  208. const ImageHeader = styled('div')`
  209. margin: -${space(4)} -${space(4)} 0 -${space(4)};
  210. border-radius: ${p => p.theme.borderRadius} ${p => p.theme.borderRadius} 0 0;
  211. background-image: url(${partnerMigrationHero});
  212. background-size: cover;
  213. background-repeat: no-repeat;
  214. background-position: top;
  215. height: 200px;
  216. @media (max-width: ${p => p.theme.breakpoints.medium}) {
  217. margin: -${space(4)} -${space(4)} 0 -${space(4)};
  218. }
  219. `;
  220. const Bullets = styled('div')`
  221. display: grid;
  222. grid-template-columns: max-content 1fr;
  223. grid-auto-rows: max-content;
  224. gap: ${space(1)} ${space(1.5)};
  225. align-items: center;
  226. font-size: ${p => p.theme.fontSizeMedium};
  227. margin-bottom: ${space(1)};
  228. `;
  229. const PathHeading = styled('h5')`
  230. font-weight: bold;
  231. margin-bottom: 0;
  232. `;
  233. const SubHeading = styled('div')`
  234. font-weight: bold;
  235. font-size: ${p => p.theme.fontSizeMedium};
  236. color: ${p => p.theme.gray300};
  237. text-transform: uppercase;
  238. `;
  239. export const modalCss = css`
  240. width: 100%;
  241. max-width: 630px;
  242. `;
  243. export default withSubscription(PartnerPlanEndingModal);