cronsBillingBanner.tsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. import styled from '@emotion/styled';
  2. import {Alert} from 'sentry/components/core/alert';
  3. import {t, tct, tn} from 'sentry/locale';
  4. import type {Organization} from 'sentry/types/organization';
  5. import getDaysSinceDate from 'sentry/utils/getDaysSinceDate';
  6. import {useApiQuery} from 'sentry/utils/queryClient';
  7. import {
  8. CronsBannerOnDemandCTA,
  9. CronsBannerUpgradeCTA,
  10. } from 'getsentry/components/crons/cronsBannerUpgradeCTA';
  11. import withSubscription from 'getsentry/components/withSubscription';
  12. import {useBillingConfig} from 'getsentry/hooks/useBillingConfig';
  13. import {
  14. type BillingConfig,
  15. type MonitorCountResponse,
  16. PlanTier,
  17. type Subscription,
  18. } from 'getsentry/types';
  19. import {getTrialDaysLeft} from 'getsentry/utils/billing';
  20. interface Props {
  21. organization: Organization;
  22. subscription: Subscription;
  23. }
  24. interface CronsPricingInfo {
  25. onDemandPrice?: number;
  26. reserved?: number;
  27. }
  28. function getCronsPricingInfo(config?: BillingConfig): CronsPricingInfo {
  29. const bucket = config?.planList.find(plan => plan.id === 'am2_business')?.planCategories
  30. .monitorSeats?.[0];
  31. return {
  32. onDemandPrice: bucket?.onDemandPrice,
  33. reserved: bucket?.events,
  34. };
  35. }
  36. /** @internal exported for tests only */
  37. export function CronsBillingBanner({organization, subscription}: Props) {
  38. const hasBillingAccess = organization.access.includes('org:billing');
  39. const trialDaysLeft = getTrialDaysLeft(subscription);
  40. const daysSinceTrial = getDaysSinceDate(subscription.lastTrialEnd ?? '');
  41. const {data: billingConfig} = useBillingConfig({organization, subscription});
  42. const {onDemandPrice, reserved} = getCronsPricingInfo(billingConfig);
  43. const queryKey = [`/organizations/${organization.slug}/monitor-count/`] as const;
  44. const {data, isPending} = useApiQuery<MonitorCountResponse>(queryKey, {
  45. staleTime: 0,
  46. });
  47. if (!data || isPending || !subscription.canSelfServe || !onDemandPrice || !reserved) {
  48. return null;
  49. }
  50. // Show alert for when we have disabled all monitors due to insufficient on-demand
  51. if (
  52. data.enabledMonitorCount === 0 &&
  53. data.overQuotaMonitorCount > 0 &&
  54. subscription.planDetails.allowOnDemand
  55. ) {
  56. return (
  57. <InsufficentOnDemandMonitorsDisabledBanner
  58. hasBillingAccess={hasBillingAccess}
  59. subscription={subscription}
  60. />
  61. );
  62. }
  63. // Subtract number of free monitors from active count
  64. const currentUsage = ((data.enabledMonitorCount - reserved) * onDemandPrice) / 100;
  65. if (currentUsage <= 0) {
  66. return null;
  67. }
  68. if (trialDaysLeft <= 7 && subscription.isTrial) {
  69. return (
  70. <TrialEndingBanner
  71. hasBillingAccess={hasBillingAccess}
  72. currentUsage={currentUsage}
  73. trialDaysLeft={trialDaysLeft}
  74. subscription={subscription}
  75. />
  76. );
  77. }
  78. // If the user's trial has ended and they aren't on a plan with on-demand
  79. if (
  80. daysSinceTrial >= 0 &&
  81. daysSinceTrial <= 7 &&
  82. !subscription.planDetails.allowOnDemand
  83. ) {
  84. return <TrialEndedBanner hasBillingAccess={hasBillingAccess} />;
  85. }
  86. return null;
  87. }
  88. interface BannerProps {
  89. hasBillingAccess: boolean;
  90. }
  91. interface TrialEndingBannerProps extends BannerProps {
  92. currentUsage: number;
  93. trialDaysLeft: number;
  94. }
  95. function TrialEndingBanner({
  96. hasBillingAccess,
  97. currentUsage,
  98. trialDaysLeft,
  99. subscription,
  100. }: TrialEndingBannerProps & {subscription: Subscription}) {
  101. const budgetType =
  102. subscription.planTier === PlanTier.AM3 ? 'pay-as-you-go' : 'on-demand';
  103. return (
  104. <TrialBanner hasBillingAccess={hasBillingAccess}>
  105. {hasBillingAccess
  106. ? tct(
  107. "Your organization's free business trial ends in [trialDaysLeft]. To continue monitoring your cron jobs, make sure your [budgetType] budget is set to a minimum of $[currentUsage].",
  108. {
  109. trialDaysLeft: tn('%s day', '%s days', trialDaysLeft),
  110. currentUsage,
  111. budgetType,
  112. }
  113. )
  114. : tct(
  115. "Your organization's free business trial ends in [trialDaysLeft]. To continue monitoring your cron jobs, ask your organization's owner or billing manager to set an [budgetType] budget for cron monitoring.",
  116. {trialDaysLeft: tn('%s day', '%s days', trialDaysLeft), budgetType}
  117. )}
  118. </TrialBanner>
  119. );
  120. }
  121. function TrialEndedBanner({hasBillingAccess}: BannerProps) {
  122. return (
  123. <TrialBanner hasBillingAccess={hasBillingAccess}>
  124. {hasBillingAccess
  125. ? t(
  126. 'Your free business trial has ended. One cron job monitor is included in your current plan. If you want to monitor more than one cron job, please increase your on-demand budget.'
  127. )
  128. : t(
  129. "Your free business trial has ended. One cron job monitor is included in your current plan. If you want to monitor more than one cron job, please ask your organization's owner or billing manager to set up an on-demand budget for cron monitoring."
  130. )}
  131. </TrialBanner>
  132. );
  133. }
  134. function TrialBanner({
  135. hasBillingAccess,
  136. children,
  137. }: BannerProps & {children?: React.ReactNode}) {
  138. return (
  139. <Alert.Container>
  140. <NoBorderRadiusAlert
  141. type="warning"
  142. showIcon
  143. trailingItems={<CronsBannerUpgradeCTA hasBillingAccess={hasBillingAccess} />}
  144. >
  145. {children}
  146. </NoBorderRadiusAlert>
  147. </Alert.Container>
  148. );
  149. }
  150. function InsufficentOnDemandMonitorsDisabledBanner({
  151. hasBillingAccess,
  152. subscription,
  153. }: BannerProps & {subscription: Subscription}) {
  154. const budgetType =
  155. subscription.planTier === PlanTier.AM3 ? 'pay-as-you-go' : 'on-demand';
  156. return (
  157. <Alert.Container>
  158. <NoBorderRadiusAlert
  159. type="warning"
  160. showIcon
  161. trailingItems={
  162. <CronsBannerOnDemandCTA
  163. hasBillingAccess={hasBillingAccess}
  164. subscription={subscription}
  165. />
  166. }
  167. >
  168. {hasBillingAccess
  169. ? tct(
  170. "Your organization doesn't have sufficient [budgetType] budget to cover your active cron job monitors. To continue monitoring your jobs, increase your [budgetType] budget or reduce your active monitors.",
  171. {budgetType}
  172. )
  173. : tct(
  174. "Your organization doesn't have sufficient [budgetType] budget to cover your active cron job monitors. To continue monitoring your jobs, ask your organization's owner or billing manager to increase your [budgetType] budget or reduce your active monitors.",
  175. {budgetType}
  176. )}
  177. </NoBorderRadiusAlert>
  178. </Alert.Container>
  179. );
  180. }
  181. export default withSubscription(CronsBillingBanner, {
  182. noLoader: true,
  183. });
  184. const NoBorderRadiusAlert = styled(Alert)`
  185. border-radius: 0;
  186. `;