contractSelect.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. import {Component} from 'react';
  2. import styled from '@emotion/styled';
  3. import {Button} from 'sentry/components/button';
  4. import {Alert} from 'sentry/components/core/alert';
  5. import Panel from 'sentry/components/panels/panel';
  6. import PanelBody from 'sentry/components/panels/panelBody';
  7. import PanelFooter from 'sentry/components/panels/panelFooter';
  8. import {t, tct} from 'sentry/locale';
  9. import {space} from 'sentry/styles/space';
  10. import {ANNUAL, MONTHLY} from 'getsentry/constants';
  11. import type {Plan} from 'getsentry/types';
  12. import PlanSelectRow from 'getsentry/views/amCheckout/steps/planSelectRow';
  13. import StepHeader from 'getsentry/views/amCheckout/steps/stepHeader';
  14. import type {StepProps} from 'getsentry/views/amCheckout/types';
  15. import {getReservedTotal} from 'getsentry/views/amCheckout/utils';
  16. type Props = StepProps;
  17. class ContractSelect extends Component<Props> {
  18. get title() {
  19. return t('Contract Term & Discounts');
  20. }
  21. /**
  22. * Filter for monthly and annual billing intervals
  23. * of the same base plan. AM1 plans can either be
  24. * monthly or annual-up-front.
  25. */
  26. get planOptions() {
  27. const {billingConfig, formData} = this.props;
  28. const basePlan = formData.plan.replace('_auf', '');
  29. const plans = billingConfig.planList.filter(({id}) => id.indexOf(basePlan) === 0);
  30. if (!plans) {
  31. throw new Error('Cannot get billing interval options');
  32. }
  33. return plans;
  34. }
  35. get annualContractWarning() {
  36. return (
  37. <ContractAlert type="info">
  38. {t(
  39. 'You are currently on an annual contract so any subscription downgrades will take effect at the end of your contract period.'
  40. )}
  41. </ContractAlert>
  42. );
  43. }
  44. isAnnual(plan: Plan) {
  45. return plan.billingInterval === ANNUAL;
  46. }
  47. getOptionName(isAnnual: boolean) {
  48. return isAnnual ? t('Annual Contract') : t('Monthly');
  49. }
  50. getDescription(isAnnual: boolean) {
  51. const {billingConfig} = this.props;
  52. return isAnnual
  53. ? tct('Save an additional [annualDiscount]% by committing to a 12-month plan', {
  54. annualDiscount: billingConfig.annualDiscount * 100,
  55. })
  56. : t('Month-to-month contract');
  57. }
  58. getPriceHeader(isAnnual: boolean) {
  59. return tct('Per [billingInterval]', {
  60. billingInterval: isAnnual ? 'year' : 'month',
  61. });
  62. }
  63. renderBody = () => {
  64. const {onUpdate, formData, subscription, promotion} = this.props;
  65. return (
  66. <PanelBody data-test-id={this.title}>
  67. {this.planOptions.map(plan => {
  68. const isSelected = plan.id === formData.plan;
  69. const isAnnual = this.isAnnual(plan);
  70. const name = this.getOptionName(isAnnual);
  71. const description = this.getDescription(isAnnual);
  72. const priceHeader = this.getPriceHeader(isAnnual);
  73. const hasWarning =
  74. isSelected &&
  75. plan.contractInterval === MONTHLY &&
  76. subscription.contractInterval === ANNUAL &&
  77. subscription.partner?.partnership.id !== 'FL';
  78. const discountData: {
  79. amount?: number;
  80. discountType?: string;
  81. } = {};
  82. if (
  83. promotion?.showDiscountInfo &&
  84. promotion.discountInfo &&
  85. // contract intervial needs to match the discount interval
  86. promotion.discountInfo.billingInterval === plan.contractInterval
  87. ) {
  88. discountData.discountType = promotion.discountInfo.discountType;
  89. discountData.amount = promotion.discountInfo.amount;
  90. }
  91. const price = getReservedTotal({
  92. plan,
  93. reserved: formData.reserved,
  94. ...discountData,
  95. });
  96. return (
  97. <PlanSelectRow
  98. key={plan.id}
  99. plan={plan}
  100. isSelected={isSelected}
  101. onUpdate={onUpdate}
  102. planValue={plan.billingInterval}
  103. planName={name}
  104. planContent={{description, features: {}}}
  105. priceHeader={priceHeader}
  106. price={price}
  107. planWarning={hasWarning ? this.annualContractWarning : undefined}
  108. />
  109. );
  110. })}
  111. </PanelBody>
  112. );
  113. };
  114. renderFooter = () => {
  115. const {stepNumber, onCompleteStep} = this.props;
  116. return (
  117. <StepFooter data-test-id={this.title}>
  118. <Button priority="primary" onClick={() => onCompleteStep(stepNumber)}>
  119. {t('Continue')}
  120. </Button>
  121. </StepFooter>
  122. );
  123. };
  124. render() {
  125. const {isActive, stepNumber, isCompleted, onEdit} = this.props;
  126. return (
  127. <Panel>
  128. <StepHeader
  129. canSkip
  130. title={this.title}
  131. isActive={isActive}
  132. stepNumber={stepNumber}
  133. isCompleted={isCompleted}
  134. onEdit={onEdit}
  135. />
  136. {isActive && this.renderBody()}
  137. {isActive && this.renderFooter()}
  138. </Panel>
  139. );
  140. }
  141. }
  142. const ContractAlert = styled(Alert)`
  143. margin: ${space(2)} 0 0;
  144. `;
  145. const StepFooter = styled(PanelFooter)`
  146. padding: ${space(2)};
  147. display: grid;
  148. align-items: center;
  149. justify-content: end;
  150. `;
  151. export default ContractSelect;