checkoutOverviewV2.tsx 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. import {Component, Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import {Tag} from 'sentry/components/core/badge/tag';
  4. import Panel from 'sentry/components/panels/panel';
  5. import PanelBody from 'sentry/components/panels/panelBody';
  6. import QuestionTooltip from 'sentry/components/questionTooltip';
  7. import {t, tct} from 'sentry/locale';
  8. import {space} from 'sentry/styles/space';
  9. import {DataCategory} from 'sentry/types/core';
  10. import type {Organization} from 'sentry/types/organization';
  11. import type {BillingConfig, Plan, Promotion, Subscription} from 'getsentry/types';
  12. import {formatReservedWithUnits} from 'getsentry/utils/billing';
  13. import {getPlanCategoryName, getSingularCategoryName} from 'getsentry/utils/dataCategory';
  14. import type {CheckoutFormData} from 'getsentry/views/amCheckout/types';
  15. import * as utils from 'getsentry/views/amCheckout/utils';
  16. type Props = {
  17. activePlan: Plan;
  18. billingConfig: BillingConfig;
  19. formData: CheckoutFormData;
  20. onUpdate: (data: any) => void;
  21. organization: Organization;
  22. subscription: Subscription;
  23. discountInfo?: Promotion['discountInfo'];
  24. };
  25. class CheckoutOverviewV2 extends Component<Props> {
  26. get shortInterval() {
  27. const {activePlan} = this.props;
  28. return utils.getShortInterval(activePlan.billingInterval);
  29. }
  30. renderPlanDetails = () => {
  31. const {activePlan} = this.props;
  32. return (
  33. <div>
  34. <Subtitle>{t('Plan Type')}</Subtitle>
  35. <SpaceBetweenRow>
  36. <div>
  37. <Title>{tct('Sentry [name] Plan', {name: activePlan.name})}</Title>
  38. <Description>
  39. {t(
  40. 'This is your standard %s subscription charge.',
  41. activePlan.billingInterval === 'annual' ? 'yearly' : 'monthly'
  42. )}
  43. </Description>
  44. </div>
  45. <Title>
  46. {utils.displayPrice({cents: activePlan.totalPrice})}
  47. {`/${this.shortInterval}`}
  48. </Title>
  49. </SpaceBetweenRow>
  50. </div>
  51. );
  52. };
  53. renderPayAsYouGoBudget = (paygBudgetTotal: number) => {
  54. if (paygBudgetTotal === 0) {
  55. return null;
  56. }
  57. return (
  58. <Fragment>
  59. <div>
  60. <Subtitle>{t('Additional Coverage')}</Subtitle>
  61. <SpaceBetweenRow style={{alignItems: 'start'}}>
  62. <Column>
  63. <Title>{t('Pay-as-you-go (PAYG) Budget')}</Title>
  64. <Description>
  65. {t('Charges are applied at the end of your monthly usage cycle.')}
  66. </Description>
  67. </Column>
  68. <Column>
  69. <Title>
  70. {t('up to ')}
  71. {`${utils.displayPrice({cents: paygBudgetTotal})}/mo`}
  72. </Title>
  73. </Column>
  74. </SpaceBetweenRow>
  75. </div>
  76. <Separator />
  77. </Fragment>
  78. );
  79. };
  80. renderReservedVolumes = () => {
  81. const {formData, activePlan} = this.props;
  82. const paygCategories = [
  83. DataCategory.MONITOR_SEATS,
  84. DataCategory.PROFILE_DURATION,
  85. DataCategory.UPTIME,
  86. ];
  87. return (
  88. <Section>
  89. <Subtitle>
  90. {t('Monthly Reserved Volumes ')}
  91. <QuestionTooltip
  92. title={t('Prepay for usage by reserving volumes and save up to 20%')}
  93. position="bottom"
  94. size="xs"
  95. />
  96. </Subtitle>
  97. <ReservedVolumes>
  98. {activePlan.checkoutCategories.map(category => {
  99. const eventBucket = utils.getBucket({
  100. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  101. events: formData.reserved[category],
  102. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  103. buckets: activePlan.planCategories[category],
  104. });
  105. const price = utils.displayPrice({cents: eventBucket.price});
  106. const isMoreThanIncluded =
  107. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  108. formData.reserved[category] > activePlan.planCategories[category][0].events;
  109. return (
  110. <SpaceBetweenRow
  111. key={category}
  112. data-test-id={`${category}-reserved`}
  113. style={{alignItems: 'center'}}
  114. >
  115. <ReservedItem>
  116. <EmphasisText>
  117. {
  118. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  119. formatReservedWithUnits(formData.reserved[category], category)
  120. }
  121. </EmphasisText>{' '}
  122. {
  123. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  124. formData.reserved[category] === 1
  125. ? getSingularCategoryName({
  126. plan: activePlan,
  127. category,
  128. })
  129. : getPlanCategoryName({plan: activePlan, category})
  130. }
  131. {paygCategories.includes(category as DataCategory) ? (
  132. <QuestionTooltip
  133. size="xs"
  134. title={t(
  135. "%s use your pay-as-you-go budget. You'll only be charged for actual usage.",
  136. getPlanCategoryName({plan: activePlan, category})
  137. )}
  138. />
  139. ) : null}
  140. </ReservedItem>
  141. <Price>
  142. {isMoreThanIncluded ? (
  143. `+ ${price}/${this.shortInterval}`
  144. ) : (
  145. <Tag>{t('Included')}</Tag>
  146. )}
  147. </Price>
  148. </SpaceBetweenRow>
  149. );
  150. })}
  151. </ReservedVolumes>
  152. </Section>
  153. );
  154. };
  155. renderTotals = (committedTotal: number, paygMonthlyBudget: number) => {
  156. const {activePlan} = this.props;
  157. return (
  158. <div>
  159. <SpaceBetweenRow>
  160. <Title style={{lineHeight: 2}}>
  161. {tct('Billed [interval]', {
  162. interval: activePlan.billingInterval === 'annual' ? 'Annually' : 'Monthly',
  163. })}
  164. </Title>
  165. <Column>
  166. <TotalPrice>{`${utils.displayPrice({cents: committedTotal})}/${this.shortInterval}`}</TotalPrice>
  167. </Column>
  168. </SpaceBetweenRow>
  169. {paygMonthlyBudget > 0 ? (
  170. <AdditionalMonthlyCharge data-test-id="additional-monthly-charge">
  171. {tct('+ up to [monthlyMax] based on PAYG usage', {
  172. monthlyMax: (
  173. <EmphasisText>{`${utils.displayPrice({cents: paygMonthlyBudget})}/mo`}</EmphasisText>
  174. ),
  175. })}{' '}
  176. <QuestionTooltip
  177. size="xs"
  178. title={t(
  179. "This is your pay-as-you-go budget, which ensures continued monitoring after you've used up your reserved event volume. We’ll only charge you for actual usage, so this is your maximum charge for overage."
  180. )}
  181. position="bottom"
  182. />
  183. </AdditionalMonthlyCharge>
  184. ) : null}
  185. </div>
  186. );
  187. };
  188. render() {
  189. const {formData, activePlan} = this.props;
  190. const committedTotal = utils.getReservedPriceCents({...formData, plan: activePlan});
  191. const paygMonthlyBudget = formData.onDemandMaxSpend || 0;
  192. return (
  193. <StyledPanel data-test-id="checkout-overview-v2">
  194. {this.renderPlanDetails()}
  195. <Separator />
  196. {this.renderPayAsYouGoBudget(paygMonthlyBudget)}
  197. {this.renderReservedVolumes()}
  198. <Separator />
  199. {this.renderTotals(committedTotal, paygMonthlyBudget)}
  200. </StyledPanel>
  201. );
  202. }
  203. }
  204. const StyledPanel = styled(Panel)`
  205. display: grid;
  206. grid-template-rows: repeat(2, auto);
  207. gap: ${space(1.5)};
  208. padding: ${space(2)} ${space(2)} ${space(4)};
  209. `;
  210. const Column = styled('div')`
  211. display: grid;
  212. grid-template-rows: repeat(2, auto);
  213. `;
  214. const Description = styled('div')`
  215. color: ${p => p.theme.subText};
  216. font-size: ${p => p.theme.fontSizeSmall};
  217. `;
  218. const SpaceBetweenRow = styled('div')`
  219. display: grid;
  220. grid-auto-flow: column;
  221. justify-content: space-between;
  222. gap: ${space(4)};
  223. `;
  224. const Title = styled('div')`
  225. font-size: ${p => p.theme.fontSizeLarge};
  226. font-weight: 600;
  227. color: ${p => p.theme.textColor};
  228. `;
  229. const Subtitle = styled('div')`
  230. display: flex;
  231. align-items: center;
  232. gap: ${space(0.5)};
  233. font-size: ${p => p.theme.fontSizeSmall};
  234. font-weight: 600;
  235. color: ${p => p.theme.subText};
  236. margin-bottom: ${space(0.5)};
  237. `;
  238. const ReservedVolumes = styled('div')`
  239. display: grid;
  240. gap: ${space(1.5)};
  241. `;
  242. const ReservedItem = styled(Title)`
  243. display: flex;
  244. gap: ${space(0.5)};
  245. align-items: center;
  246. color: ${p => p.theme.subText};
  247. `;
  248. const Section = styled(PanelBody)`
  249. color: ${p => p.theme.subText};
  250. font-size: ${p => p.theme.fontSizeLarge};
  251. `;
  252. const Separator = styled('div')`
  253. border-top: 1px solid ${p => p.theme.innerBorder};
  254. `;
  255. const Price = styled('div')`
  256. justify-self: end;
  257. color: ${p => p.theme.textColor};
  258. display: flex;
  259. justify-content: end;
  260. `;
  261. const TotalPrice = styled(Price)`
  262. font-size: ${p => p.theme.headerFontSize};
  263. font-weight: 600;
  264. `;
  265. const AdditionalMonthlyCharge = styled('div')`
  266. display: flex;
  267. align-items: center;
  268. justify-content: flex-end;
  269. gap: ${space(0.5)};
  270. font-size: ${p => p.theme.fontSizeSmall};
  271. color: ${p => p.theme.subText};
  272. `;
  273. const EmphasisText = styled('span')`
  274. color: ${p => p.theme.textColor};
  275. font-weight: 600;
  276. `;
  277. export default CheckoutOverviewV2;