gsBanner.tsx 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476
  1. import React, {Component, Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import * as Sentry from '@sentry/react';
  4. import Cookies from 'js-cookie';
  5. import moment from 'moment-timezone';
  6. import type {ModalRenderProps} from 'sentry/actionCreators/modal';
  7. import {openModal} from 'sentry/actionCreators/modal';
  8. import {fetchOrganizationDetails} from 'sentry/actionCreators/organization';
  9. import type {PromptData} from 'sentry/actionCreators/prompts';
  10. import {
  11. batchedPromptsCheck,
  12. promptsCheck,
  13. promptsUpdate,
  14. } from 'sentry/actionCreators/prompts';
  15. import type {Client} from 'sentry/api';
  16. import {Button, LinkButton} from 'sentry/components/button';
  17. import ButtonBar from 'sentry/components/buttonBar';
  18. import {Alert} from 'sentry/components/core/alert';
  19. import {Badge} from 'sentry/components/core/badge';
  20. import ExternalLink from 'sentry/components/links/externalLink';
  21. import {DATA_CATEGORY_INFO} from 'sentry/constants';
  22. import {IconClose} from 'sentry/icons';
  23. import {t, tct} from 'sentry/locale';
  24. import ConfigStore from 'sentry/stores/configStore';
  25. import GuideStore from 'sentry/stores/guideStore';
  26. import {space} from 'sentry/styles/space';
  27. import {DataCategory, DataCategoryExact} from 'sentry/types/core';
  28. import type {Organization} from 'sentry/types/organization';
  29. import {browserHistory} from 'sentry/utils/browserHistory';
  30. import {isActiveSuperuser} from 'sentry/utils/isActiveSuperuser';
  31. import {Oxfordize} from 'sentry/utils/oxfordizeArray';
  32. import {promptIsDismissed} from 'sentry/utils/promptIsDismissed';
  33. import normalizeUrl from 'sentry/utils/url/normalizeUrl';
  34. import withApi from 'sentry/utils/withApi';
  35. import {getDocsLinkForEventType} from 'sentry/views/settings/account/notifications/utils';
  36. import {
  37. openForcedTrialModal,
  38. openPartnerPlanEndingModal,
  39. openTrialEndingModal,
  40. } from 'getsentry/actionCreators/modal';
  41. import type {EventType} from 'getsentry/components/addEventsCTA';
  42. import AddEventsCTA from 'getsentry/components/addEventsCTA';
  43. import ProductTrialAlert from 'getsentry/components/productTrial/productTrialAlert';
  44. import {makeLinkToOwnersAndBillingMembers} from 'getsentry/components/profiling/alerts';
  45. import withSubscription from 'getsentry/components/withSubscription';
  46. import ZendeskLink from 'getsentry/components/zendeskLink';
  47. import SubscriptionStore from 'getsentry/stores/subscriptionStore';
  48. import {
  49. PlanTier,
  50. type Promotion,
  51. type PromotionClaimed,
  52. type Subscription,
  53. } from 'getsentry/types';
  54. import {
  55. getActiveProductTrial,
  56. getContractDaysLeft,
  57. getProductTrial,
  58. getTrialLength,
  59. hasPerformance,
  60. isBusinessTrial,
  61. partnerPlanEndingModalIsDismissed,
  62. trialPromptIsDismissed,
  63. } from 'getsentry/utils/billing';
  64. import {getSingularCategoryName} from 'getsentry/utils/dataCategory';
  65. import {getPendoAccountFields} from 'getsentry/utils/pendo';
  66. import {claimAvailablePromotion} from 'getsentry/utils/promotionUtils';
  67. import trackGetsentryAnalytics from 'getsentry/utils/trackGetsentryAnalytics';
  68. import trackMarketingEvent from 'getsentry/utils/trackMarketingEvent';
  69. import withPromotions from 'getsentry/utils/withPromotions';
  70. enum ModalType {
  71. USAGE_EXCEEDED = 'usage-exceeded',
  72. GRACE_PERIOD = 'grace-period',
  73. PAST_DUE = 'past-due',
  74. MEMBER_LIMIT = 'member-limit',
  75. }
  76. /**
  77. * how many days before the trial ends should we show the trial ending modal?
  78. */
  79. const TRIAL_ENDING_DAY_WINDOW = 3;
  80. const ALERTS_OFF: Record<EventType, boolean> = {
  81. error: false,
  82. transaction: false,
  83. replay: false,
  84. attachment: false,
  85. monitorSeat: false,
  86. span: false,
  87. profileDuration: false,
  88. uptime: false,
  89. };
  90. type SuspensionModalProps = ModalRenderProps & {
  91. subscription: Subscription;
  92. };
  93. function SuspensionModal({Header, Body, Footer, subscription}: SuspensionModalProps) {
  94. return (
  95. <Fragment>
  96. <Header>{'Action Required'}</Header>
  97. <Body>
  98. <Alert.Container>
  99. <Alert type="warning" showIcon>
  100. {t('Your account has been suspended')}
  101. </Alert>
  102. </Alert.Container>
  103. <p>{t('Your account has been suspended with the following reason:')}</p>
  104. <ul>
  105. <li>
  106. <strong>{subscription.suspensionReason}</strong>
  107. </li>
  108. </ul>
  109. <p>
  110. {t(
  111. 'Until this situation is resolved you will not be able to send events to Sentry.'
  112. )}
  113. </p>
  114. </Body>
  115. <Footer>
  116. <ZendeskLink
  117. subject="Account Suspension"
  118. className="btn btn-primary"
  119. source="account-suspension"
  120. >
  121. {t('Contact Support')}
  122. </ZendeskLink>
  123. </Footer>
  124. </Fragment>
  125. );
  126. }
  127. type NoticeModalProps = ModalRenderProps & {
  128. billingPermissions: boolean;
  129. organization: Organization;
  130. subscription: Subscription;
  131. whichModal: ModalType;
  132. };
  133. function NoticeModal({
  134. Header,
  135. Body,
  136. Footer,
  137. closeModal,
  138. subscription,
  139. organization,
  140. whichModal,
  141. billingPermissions,
  142. }: NoticeModalProps) {
  143. const closeModalAndContinue = (link: string) => {
  144. closeModal();
  145. if (whichModal === ModalType.PAST_DUE) {
  146. trackGetsentryAnalytics('billing_failure.button_clicked', {
  147. organization,
  148. has_link: true,
  149. has_permissions: billingPermissions,
  150. referrer: 'modal-billing-failure',
  151. });
  152. }
  153. if (link === window.location.pathname) {
  154. return;
  155. }
  156. browserHistory.push(link);
  157. };
  158. const closeModalDoNotContinue = () => {
  159. closeModal();
  160. if (whichModal === ModalType.PAST_DUE) {
  161. trackGetsentryAnalytics('billing_failure.button_clicked', {
  162. organization,
  163. has_link: false,
  164. has_permissions: billingPermissions,
  165. referrer: 'modal-billing-failure',
  166. });
  167. }
  168. };
  169. const alertType = whichModal === ModalType.PAST_DUE ? 'error' : 'warning';
  170. let subText: React.ReactNode;
  171. let body: React.ReactNode;
  172. let title: React.ReactNode;
  173. let link: string;
  174. let primaryButtonMessage: React.ReactNode;
  175. switch (whichModal) {
  176. case ModalType.GRACE_PERIOD:
  177. title = t('Grace period started');
  178. body = tct(
  179. `Your organization has depleted its error capacity for the current usage period.
  180. We've put your account into a one time grace period, which will continue to accept errors at a limited rate.
  181. This grace period ends on [gracePeriodEnd].`,
  182. {gracePeriodEnd: moment(subscription.gracePeriodEnd).format('ll')}
  183. );
  184. link = normalizeUrl(`/settings/${organization.slug}/billing/overview/`);
  185. primaryButtonMessage = t('Continue');
  186. break;
  187. case ModalType.USAGE_EXCEEDED:
  188. title = t('Usage exceeded');
  189. body = t(
  190. `Your organization has depleted its event capacity for the current usage period and is currently not receiving new events.`
  191. );
  192. link = normalizeUrl(`/settings/${organization.slug}/billing/overview/`);
  193. primaryButtonMessage = t('Continue');
  194. break;
  195. case ModalType.PAST_DUE:
  196. title = t('Unable to bill your account');
  197. body = billingPermissions
  198. ? t(
  199. `There was an issue with your payment. Update your payment information to ensure uniterrupted access to Sentry.`
  200. )
  201. : t(
  202. `There was an issue with your payment. Please have the Org Owner or Billing Member update your payment information to ensure continued access to Sentry.`
  203. );
  204. link = billingPermissions
  205. ? normalizeUrl(
  206. `/settings/${organization.slug}/billing/details/?referrer=banner-billing-failure`
  207. )
  208. : makeLinkToOwnersAndBillingMembers(organization, 'past_due_modal-alert');
  209. primaryButtonMessage = billingPermissions
  210. ? t('Update Billing Details')
  211. : t('See Who Can Update');
  212. break;
  213. case ModalType.MEMBER_LIMIT:
  214. title = t('Member limit exceeded');
  215. body = t(
  216. `You organization has more members than your current subscription
  217. allows. You will need to upgrade your subscription to ensure everyone
  218. has access to Sentry.`
  219. );
  220. link = normalizeUrl(`/settings/${organization.slug}/billing/overview/`);
  221. primaryButtonMessage = t('Continue');
  222. break;
  223. default:
  224. }
  225. if (subscription.usageExceeded || subscription.isGracePeriod) {
  226. if (subscription.isFree) {
  227. subText = subscription.canTrial
  228. ? t(
  229. `Not yet ready to upgrade? You can start a free %s-day trial with
  230. unlimited events to better understand your usage.`,
  231. getTrialLength(organization)
  232. )
  233. : t('To ensure uninterrupted service, upgrade your subscription.');
  234. } else {
  235. if (subscription.planTier === PlanTier.AM3) {
  236. subText = t(
  237. `To ensure uninterrupted service, upgrade your subscription or increase your pay-as-you-go spend limit.`
  238. );
  239. } else {
  240. subText = t(
  241. `To ensure uninterrupted service, upgrade your subscription or increase your on-demand spend limit.`
  242. );
  243. }
  244. }
  245. }
  246. return (
  247. <Fragment>
  248. <Header data-test-id={`modal-${whichModal}`}>
  249. <h4>{t('Action Required')}</h4>
  250. </Header>
  251. <Body>
  252. <Alert.Container>
  253. <Alert type={alertType} showIcon>
  254. {title}
  255. </Alert>
  256. </Alert.Container>
  257. <p>{body}</p>
  258. {subText && <p>{subText}</p>}
  259. </Body>
  260. <Footer>
  261. <Button onClick={() => closeModalDoNotContinue()}>{t('Remind Me Later')}</Button>
  262. <Button
  263. priority="primary"
  264. onClick={() => closeModalAndContinue(link)}
  265. style={{marginLeft: space(2)}}
  266. data-test-id="modal-continue-button"
  267. >
  268. {primaryButtonMessage}
  269. </Button>
  270. </Footer>
  271. </Fragment>
  272. );
  273. }
  274. type Props = {
  275. api: Client;
  276. isLoading: boolean;
  277. organization: Organization;
  278. promotionData: {
  279. activePromotions: PromotionClaimed[];
  280. availablePromotions: Promotion[];
  281. completedPromotions: PromotionClaimed[];
  282. };
  283. subscription: Subscription;
  284. };
  285. type State = {
  286. deactivatedMemberDismissed: boolean;
  287. overageAlertDismissed: {[key in EventType]: boolean};
  288. overageWarningDismissed: {[key in EventType]: boolean};
  289. productTrialDismissed: {[key in EventType]: boolean};
  290. };
  291. class GSBanner extends Component<Props, State> {
  292. // assume dismissed until we've checked the backend
  293. state: State = {
  294. deactivatedMemberDismissed: true,
  295. overageAlertDismissed: {
  296. error: true,
  297. transaction: true,
  298. replay: true,
  299. attachment: true,
  300. monitorSeat: true,
  301. span: true,
  302. profileDuration: true,
  303. uptime: true,
  304. },
  305. overageWarningDismissed: {
  306. error: true,
  307. transaction: true,
  308. replay: true,
  309. attachment: true,
  310. monitorSeat: true,
  311. span: true,
  312. profileDuration: true,
  313. uptime: true,
  314. },
  315. productTrialDismissed: {
  316. error: true,
  317. transaction: true,
  318. replay: true,
  319. attachment: true,
  320. monitorSeat: true,
  321. span: true,
  322. profileDuration: true,
  323. uptime: true,
  324. },
  325. };
  326. async componentDidMount() {
  327. if (this.props.promotionData) {
  328. this.activateFirstAvailablePromo()
  329. .then(() => this.initializePendo())
  330. .catch(Sentry.captureException);
  331. }
  332. if (this.props.organization.access.length > 0) {
  333. this.tryTriggerTrialEndingModal();
  334. this.tryTriggerSuspendedModal();
  335. this.tryTriggerNoticeModal();
  336. this.tryTriggerForcedTrial();
  337. this.tryTriggerForcedTrialModal();
  338. this.tryTriggerPartnerPlanEndingModal();
  339. }
  340. await this.checkPrompts();
  341. // must happen after prompts check
  342. if (this.overageAlertType !== null) {
  343. const {organization, subscription} = this.props;
  344. const isWarning = this.overageAlertType === 'warning';
  345. const eventTypes = Object.entries(
  346. isWarning ? this.overageWarningActive : this.overageAlertActive
  347. )
  348. .filter(([_, value]) => value)
  349. .map(([key, _]) => key as EventType);
  350. trackGetsentryAnalytics('quota_alert.alert_displayed', {
  351. organization,
  352. subscription,
  353. event_types: eventTypes.sort().join(','),
  354. is_warning: isWarning,
  355. });
  356. }
  357. }
  358. componentDidUpdate(prevProps: Props) {
  359. if (this.props.promotionData !== prevProps.promotionData) {
  360. this.activateFirstAvailablePromo()
  361. .then(() => this.initializePendo())
  362. .catch(Sentry.captureException);
  363. }
  364. }
  365. get trialEndMoment() {
  366. return moment().add(TRIAL_ENDING_DAY_WINDOW, 'days');
  367. }
  368. get hasBillingPerms() {
  369. return this.props.organization?.access?.includes('org:billing');
  370. }
  371. async activateFirstAvailablePromo() {
  372. const {organization, promotionData, isLoading} = this.props;
  373. if (!isLoading && promotionData) {
  374. if (isActiveSuperuser()) {
  375. return;
  376. }
  377. await claimAvailablePromotion({
  378. promotionData,
  379. organization,
  380. });
  381. }
  382. }
  383. async initializePendo() {
  384. const {organization, subscription} = this.props;
  385. if (!window.pendo || typeof window.pendo.initialize !== 'function') {
  386. return;
  387. }
  388. try {
  389. const data = await this.props.api.requestPromise(
  390. `/organizations/${organization.slug}/pendo-details/`
  391. );
  392. const activePromotions = this.props.promotionData?.activePromotions;
  393. const completedPromotions = this.props.promotionData?.completedPromotions;
  394. const user = ConfigStore.get('user');
  395. // if there is a current guide active, delay Pendo until it's done
  396. // if no current active guide, can just start Pendo
  397. // TODO: should delay Pendo if there is any popup at all that's blocking and not just guides
  398. const guideIsActive = !!GuideStore.state.currentGuide;
  399. window.pendo.initialize({
  400. guides: {
  401. delay: guideIsActive,
  402. },
  403. visitor: {
  404. id: `${organization.id}.${user.id}`, // need uniqueness per org per user
  405. userId: user.id,
  406. role: organization.orgRole,
  407. isDarkMode: ConfigStore.get('theme') === 'dark',
  408. ...data.userDetails,
  409. },
  410. account: {
  411. id: organization.id,
  412. ...getPendoAccountFields(subscription, organization, {
  413. activePromotions,
  414. completedPromotions,
  415. }),
  416. ...data.organizationDetails,
  417. },
  418. });
  419. } catch (err) {
  420. // server will catch any 500 errors that need attention
  421. return;
  422. }
  423. }
  424. tryTriggerTrialEndingModal() {
  425. const {organization, subscription} = this.props;
  426. const trialEndingWindow = [moment(), this.trialEndMoment] as const;
  427. // Only show the trial notice if the user is on a business plan trial
  428. // Performance trials would require different content not currently supported
  429. const showTrialEndedNotice =
  430. !subscription.hasDismissedTrialEndingNotice &&
  431. subscription.canSelfServe &&
  432. isBusinessTrial(subscription) &&
  433. moment(subscription.trialEnd).isBetween(...trialEndingWindow);
  434. if (!showTrialEndedNotice) {
  435. return;
  436. }
  437. openTrialEndingModal({organization});
  438. }
  439. async tryTriggerPartnerPlanEndingModal() {
  440. const {organization, subscription, api} = this.props;
  441. const hasPartnerMigrationFeature = organization.features.includes(
  442. 'partner-billing-migration'
  443. );
  444. const hasPendingUpgrade =
  445. subscription.pendingChanges !== null &&
  446. subscription.pendingChanges?.planDetails.price > 0;
  447. const daysLeft = getContractDaysLeft(subscription);
  448. const showPartnerPlanEndingNotice =
  449. subscription.partner !== null &&
  450. !hasPendingUpgrade &&
  451. daysLeft >= 0 &&
  452. daysLeft <= 30 &&
  453. subscription.partner.isActive &&
  454. hasPartnerMigrationFeature;
  455. if (!showPartnerPlanEndingNotice) {
  456. return;
  457. }
  458. let hasDismissed = true;
  459. const prompt = await promptsCheck(api, {
  460. organization,
  461. feature: 'partner_plan_ending_modal',
  462. });
  463. if (daysLeft > 7) {
  464. hasDismissed = partnerPlanEndingModalIsDismissed(prompt, subscription, 'month');
  465. } else if (daysLeft > 2) {
  466. hasDismissed = partnerPlanEndingModalIsDismissed(prompt, subscription, 'week');
  467. } else if (daysLeft > 0) {
  468. hasDismissed = partnerPlanEndingModalIsDismissed(prompt, subscription, 'two');
  469. } else if (daysLeft === 0) {
  470. hasDismissed = partnerPlanEndingModalIsDismissed(prompt, subscription, 'zero');
  471. }
  472. if (!hasDismissed) {
  473. openPartnerPlanEndingModal({organization, subscription});
  474. }
  475. }
  476. tryTriggerSuspendedModal() {
  477. const {subscription} = this.props;
  478. if (!subscription.isSuspended) {
  479. return;
  480. }
  481. openModal(props => <SuspensionModal {...props} subscription={subscription} />);
  482. }
  483. tryTriggerNoticeModal() {
  484. const {organization, subscription} = this.props;
  485. const whichModal = subscription.isGracePeriod
  486. ? ModalType.GRACE_PERIOD
  487. : subscription.usageExceeded
  488. ? ModalType.USAGE_EXCEEDED
  489. : subscription.isPastDue && subscription.canSelfServe
  490. ? ModalType.PAST_DUE
  491. : null;
  492. if (whichModal === null) {
  493. return;
  494. }
  495. // Only show USAGE_EXCEEDED or PAST_DUE for members
  496. if (
  497. !this.hasBillingPerms &&
  498. !(ModalType.USAGE_EXCEEDED || whichModal === ModalType.PAST_DUE)
  499. ) {
  500. return;
  501. }
  502. const cookie = Cookies.get('gsb');
  503. // Did they already see the modal?
  504. if (cookie?.split(',').includes(subscription.slug)) {
  505. return;
  506. }
  507. const modalAnalytics = {
  508. [ModalType.GRACE_PERIOD]: 'grace_period_modal.seen',
  509. [ModalType.USAGE_EXCEEDED]: 'usage_exceeded_modal.seen',
  510. [ModalType.PAST_DUE]: 'past_due_modal.seen',
  511. } as const;
  512. const eventKey = modalAnalytics[whichModal];
  513. const billingPermissions = this.hasBillingPerms;
  514. if (eventKey) {
  515. trackGetsentryAnalytics(eventKey, {organization, subscription});
  516. }
  517. if (eventKey === 'past_due_modal.seen') {
  518. trackGetsentryAnalytics('billing_failure.displayed_banner', {
  519. organization,
  520. has_permissions: billingPermissions,
  521. referrer: 'banner-billing-failure',
  522. });
  523. }
  524. const onClose = () => {
  525. let value = subscription.slug;
  526. if (cookie && !cookie.includes(value)) {
  527. value = `${cookie},${value}`;
  528. }
  529. const expires = new Date();
  530. expires.setDate(expires.getDate() + 1);
  531. document.cookie = `gsb=${value}; expires=${expires.toUTCString()}; path=/`;
  532. };
  533. openModal(
  534. props => (
  535. <NoticeModal
  536. {...props}
  537. {...{organization, subscription, whichModal, billingPermissions}}
  538. />
  539. ),
  540. {onClose}
  541. );
  542. }
  543. async tryTriggerForcedTrial() {
  544. const {organization, subscription, api} = this.props;
  545. const user = ConfigStore.get('user');
  546. // check for required conditions of triggering a forced trial of any type
  547. const considerTrigger =
  548. subscription.canSelfServe && // must be self serve
  549. subscription.isFree &&
  550. hasPerformance(subscription.planDetails) &&
  551. !subscription.isExemptFromForcedTrial && // orgs who ever did enterprise trials are exempt
  552. !user?.isSuperuser; // never trigger for superusers
  553. if (!considerTrigger) {
  554. return;
  555. }
  556. // mutliple possible trial endpoints depending on the situation
  557. let endpoint: string;
  558. // check for restricted integration
  559. if (subscription.hasRestrictedIntegration) {
  560. endpoint = `/organizations/${organization.slug}/restricted-integration-trial/`;
  561. // only trigger if member limit is 1 and we have multiple licenses used
  562. } else if (subscription.totalLicenses === 1 && subscription.usedLicenses > 1) {
  563. endpoint = `/organizations/${organization.slug}/over-member-limit-trial/`;
  564. } else {
  565. return;
  566. }
  567. try {
  568. await api.requestPromise(endpoint, {
  569. method: 'POST',
  570. });
  571. trackMarketingEvent('Start Trial');
  572. // Refresh organization and subscription state
  573. // do not mark the trial since we have this modal
  574. SubscriptionStore.loadData(organization.slug, null);
  575. fetchOrganizationDetails(api, organization.slug);
  576. openForcedTrialModal({organization});
  577. } catch (error) {
  578. // let check fail but capture exception
  579. Sentry.captureException(error);
  580. }
  581. }
  582. tryTriggerForcedTrialModal() {
  583. const {subscription, organization} = this.props;
  584. if (
  585. subscription.isTrial &&
  586. subscription.isForcedTrial &&
  587. !subscription.hasDismissedForcedTrialNotice
  588. ) {
  589. openForcedTrialModal({organization});
  590. }
  591. }
  592. async checkPrompts() {
  593. const {api, organization, subscription} = this.props;
  594. try {
  595. const checkResults = await batchedPromptsCheck(
  596. api,
  597. [
  598. 'deactivated_member_alert',
  599. // overage alerts
  600. 'errors_overage_alert',
  601. 'attachments_overage_alert',
  602. 'transactions_overage_alert',
  603. 'replays_overage_alert',
  604. 'monitor_seats_overage_alert',
  605. 'spans_overage_alert',
  606. 'profile_duration_overage_alert',
  607. 'uptime_overage_alert',
  608. // warning alerts
  609. 'errors_warning_alert',
  610. 'attachments_warning_alert',
  611. 'transactions_warning_alert',
  612. 'replays_warning_alert',
  613. 'monitor_seats_warning_alert',
  614. 'spans_warning_alert',
  615. 'profile_duration_warning_alert',
  616. 'uptime_warning_alert',
  617. // product trial alerts
  618. 'errors_product_trial_alert',
  619. 'attachments_product_trial_alert',
  620. 'transactions_product_trial_alert',
  621. 'replays_product_trial_alert',
  622. 'monitor_seats_product_trial_alert',
  623. 'spans_product_trial_alert',
  624. 'profile_duration_product_trial_alert',
  625. 'uptime_product_trial_alert',
  626. ],
  627. {
  628. organization,
  629. }
  630. );
  631. // overage notifications should get reset when ondemand period ends
  632. const promptIsDismissedForBillingPeriod = (prompt: PromptData) => {
  633. const {snoozedTime, dismissedTime} = prompt || {};
  634. // TODO: dismissed prompt should always return false
  635. const time = snoozedTime || dismissedTime;
  636. if (!time) {
  637. return false;
  638. }
  639. const onDemandPeriodEnd = new Date(subscription.onDemandPeriodEnd);
  640. onDemandPeriodEnd.setHours(23, 59, 59);
  641. return time <= onDemandPeriodEnd.getTime() / 1000;
  642. };
  643. this.setState({
  644. // not billing related prompt checks
  645. deactivatedMemberDismissed: promptIsDismissed(
  646. checkResults.deactivated_member_alert!
  647. ),
  648. // billing period related prompt checks
  649. overageAlertDismissed: {
  650. error: promptIsDismissedForBillingPeriod(checkResults.errors_overage_alert!),
  651. transaction: promptIsDismissedForBillingPeriod(
  652. checkResults.transactions_overage_alert!
  653. ),
  654. replay: promptIsDismissedForBillingPeriod(checkResults.replays_overage_alert!),
  655. attachment: promptIsDismissedForBillingPeriod(
  656. checkResults.attachments_overage_alert!
  657. ),
  658. monitorSeat: promptIsDismissedForBillingPeriod(
  659. checkResults.monitor_seats_overage_alert!
  660. ),
  661. span: promptIsDismissedForBillingPeriod(checkResults.spans_overage_alert!),
  662. profileDuration: promptIsDismissedForBillingPeriod(
  663. checkResults.profile_duration_overage_alert!
  664. ),
  665. uptime: promptIsDismissedForBillingPeriod(checkResults.uptime_overage_alert!),
  666. },
  667. overageWarningDismissed: {
  668. error: promptIsDismissedForBillingPeriod(checkResults.errors_warning_alert!),
  669. transaction: promptIsDismissedForBillingPeriod(
  670. checkResults.transactions_warning_alert!
  671. ),
  672. replay: promptIsDismissedForBillingPeriod(checkResults.replays_warning_alert!),
  673. attachment: promptIsDismissedForBillingPeriod(
  674. checkResults.attachments_warning_alert!
  675. ),
  676. monitorSeat: promptIsDismissedForBillingPeriod(
  677. checkResults.monitor_seats_warning_alert!
  678. ),
  679. span: promptIsDismissedForBillingPeriod(checkResults.spans_warning_alert!),
  680. profileDuration: promptIsDismissedForBillingPeriod(
  681. checkResults.profile_duration_warning_alert!
  682. ),
  683. uptime: promptIsDismissedForBillingPeriod(checkResults.uptime_warning_alert!),
  684. },
  685. productTrialDismissed: {
  686. error: trialPromptIsDismissed(
  687. checkResults.errors_product_trial_alert!,
  688. subscription
  689. ),
  690. transaction: trialPromptIsDismissed(
  691. checkResults.transactions_product_trial_alert!,
  692. subscription
  693. ),
  694. replay: trialPromptIsDismissed(
  695. checkResults.replays_product_trial_alert!,
  696. subscription
  697. ),
  698. attachment: trialPromptIsDismissed(
  699. checkResults.attachments_product_trial_alert!,
  700. subscription
  701. ),
  702. monitorSeat: trialPromptIsDismissed(
  703. checkResults.monitor_seats_product_trial_alert!,
  704. subscription
  705. ),
  706. span: trialPromptIsDismissed(
  707. checkResults.spans_product_trial_alert!,
  708. subscription
  709. ),
  710. profileDuration: trialPromptIsDismissed(
  711. checkResults.profile_duration_product_trial_alert!,
  712. subscription
  713. ),
  714. uptime: trialPromptIsDismissed(
  715. checkResults.uptime_product_trial_alert!,
  716. subscription
  717. ),
  718. },
  719. });
  720. } catch (error) {
  721. // let check fail but capture exception
  722. Sentry.captureException(error);
  723. }
  724. }
  725. get overageAlertActive(): {[key in EventType]: boolean} {
  726. const {subscription} = this.props;
  727. if (subscription.hasOverageNotificationsDisabled) {
  728. return ALERTS_OFF;
  729. }
  730. return {
  731. error:
  732. !this.state.overageAlertDismissed.error &&
  733. !!subscription.categories.errors?.usageExceeded,
  734. transaction:
  735. !this.state.overageAlertDismissed.transaction &&
  736. !!subscription.categories.transactions?.usageExceeded,
  737. replay:
  738. !this.state.overageAlertDismissed.replay &&
  739. !!subscription.categories.replays?.usageExceeded,
  740. attachment:
  741. !this.state.overageAlertDismissed.attachment &&
  742. !!subscription.categories.attachments?.usageExceeded,
  743. monitorSeat:
  744. !this.state.overageAlertDismissed.monitorSeat &&
  745. !!subscription.categories.monitorSeats?.usageExceeded,
  746. span:
  747. !this.state.overageAlertDismissed.span &&
  748. !!subscription.categories.spans?.usageExceeded,
  749. profileDuration:
  750. !this.state.overageAlertDismissed.profileDuration &&
  751. !!subscription.categories.profileDuration?.usageExceeded,
  752. uptime:
  753. !this.state.overageAlertDismissed.uptime &&
  754. !!subscription.categories.uptime?.usageExceeded,
  755. };
  756. }
  757. get overageWarningActive(): {[key in EventType]: boolean} {
  758. const {subscription} = this.props;
  759. // disable warnings if org has on-demand
  760. if (
  761. subscription.hasOverageNotificationsDisabled ||
  762. subscription.onDemandMaxSpend > 0
  763. ) {
  764. return ALERTS_OFF;
  765. }
  766. return {
  767. error:
  768. !this.state.overageWarningDismissed.error &&
  769. !!subscription.categories.errors?.sentUsageWarning,
  770. transaction:
  771. !this.state.overageWarningDismissed.transaction &&
  772. !!subscription.categories.transactions?.sentUsageWarning,
  773. replay:
  774. !this.state.overageWarningDismissed.replay &&
  775. !!subscription.categories.replays?.sentUsageWarning,
  776. attachment:
  777. !this.state.overageWarningDismissed.attachment &&
  778. !!subscription.categories.attachments?.sentUsageWarning,
  779. monitorSeat:
  780. !this.state.overageWarningDismissed.monitorSeat &&
  781. !!subscription.categories.monitorSeats?.sentUsageWarning,
  782. span:
  783. !this.state.overageWarningDismissed.span &&
  784. !!subscription.categories.spans?.sentUsageWarning,
  785. profileDuration:
  786. !this.state.overageWarningDismissed.profileDuration &&
  787. !!subscription.categories.profileDuration?.sentUsageWarning,
  788. uptime:
  789. !this.state.overageWarningDismissed.uptime &&
  790. !!subscription.categories.uptime?.sentUsageWarning,
  791. };
  792. }
  793. // Returns true for overage alert, false for overage warning, and null if we don't show anything.
  794. get overageAlertType(): 'critical' | 'warning' | null {
  795. const {subscription} = this.props;
  796. if (!hasPerformance(subscription.planDetails)) {
  797. return null;
  798. }
  799. if (!subscription.canSelfServe) {
  800. return null;
  801. }
  802. if (Object.values(this.overageAlertActive).some(a => a)) {
  803. return 'critical';
  804. }
  805. if (Object.values(this.overageWarningActive).some(a => a)) {
  806. return 'warning';
  807. }
  808. return null;
  809. }
  810. renderOverageAlertPrimaryCTA(eventTypes: EventType[], isWarning: boolean) {
  811. const {subscription, organization} = this.props;
  812. // can't use as const with ternary
  813. const notificationType: 'overage_warning' | 'overage_critical' = isWarning
  814. ? 'overage_warning'
  815. : 'overage_critical';
  816. const props = {
  817. organization,
  818. subscription,
  819. eventTypes,
  820. notificationType,
  821. referrer: `overage-alert-${eventTypes.join('-')}`,
  822. source: isWarning ? 'quota-warning' : 'quota-overage',
  823. handleRequestSent: () => this.handleOverageSnooze(eventTypes, isWarning),
  824. };
  825. return <AddEventsCTA {...props} />;
  826. }
  827. handleOverageSnooze(eventTypes: EventType[], isWarning: boolean) {
  828. const {organization, api} = this.props;
  829. const dismissState: {[key in EventType]: boolean} = isWarning
  830. ? this.state.overageWarningDismissed
  831. : this.state.overageAlertDismissed;
  832. for (const eventType of eventTypes) {
  833. if (dismissState[eventType]) {
  834. // This type of event is already dismissed. Skip.
  835. continue;
  836. }
  837. const key = isWarning ? 'warning' : 'overage';
  838. const featureMap: Record<EventType, string> = {
  839. error: `errors_${key}_alert`,
  840. transaction: `transactions_${key}_alert`,
  841. replay: `replays_${key}_alert`,
  842. attachment: `attachments_${key}_alert`,
  843. monitorSeat: `monitor_seats_${key}_alert`,
  844. span: `spans_${key}_alert`,
  845. profileDuration: `profile_duration_${key}_alert`,
  846. uptime: `uptime_${key}_alert`,
  847. };
  848. promptsUpdate(api, {
  849. organization,
  850. feature: featureMap[eventType],
  851. status: 'snoozed',
  852. });
  853. }
  854. const dismissedState: {[key in EventType]: boolean} = {
  855. error: true,
  856. attachment: true,
  857. replay: true,
  858. transaction: true,
  859. monitorSeat: true,
  860. span: true,
  861. profileDuration: true,
  862. uptime: true,
  863. };
  864. // Suppress all warnings and alerts
  865. this.setState({
  866. overageAlertDismissed: dismissedState,
  867. overageWarningDismissed: dismissedState,
  868. });
  869. }
  870. renderOverageAlert(isWarning: boolean) {
  871. const {organization, subscription} = this.props;
  872. const plan = subscription.planDetails;
  873. let overquotaPrompt: React.ReactNode;
  874. let eventTypes: EventType[] = [];
  875. const eventTypeToElement = (eventType: EventType): JSX.Element => {
  876. const onClick = () => {
  877. trackGetsentryAnalytics('quota_alert.clicked_link', {
  878. organization,
  879. subscription,
  880. event_types: eventTypes.sort().join(','),
  881. is_warning: isWarning,
  882. clicked_event: eventType,
  883. });
  884. };
  885. // @ts-expect-error TS(2339): Property 'profileDuration' does not exist on type ... Remove this comment to see the full error message
  886. return {
  887. error: (
  888. <ExternalLink
  889. key="error"
  890. href={getDocsLinkForEventType(DataCategoryExact.ERROR)}
  891. onClick={onClick}
  892. >
  893. {getSingularCategoryName({
  894. plan,
  895. category: DataCategory.ERRORS,
  896. capitalize: false,
  897. })}
  898. </ExternalLink>
  899. ),
  900. transaction: (
  901. <ExternalLink
  902. key="transaction"
  903. href={getDocsLinkForEventType(DataCategoryExact.TRANSACTION)}
  904. onClick={onClick}
  905. >
  906. {getSingularCategoryName({
  907. plan,
  908. category: DataCategory.TRANSACTIONS,
  909. capitalize: false,
  910. })}
  911. </ExternalLink>
  912. ),
  913. replay: (
  914. <ExternalLink
  915. key="replay"
  916. href={getDocsLinkForEventType(DataCategoryExact.REPLAY)}
  917. onClick={onClick}
  918. >
  919. {getSingularCategoryName({
  920. plan,
  921. category: DataCategory.REPLAYS,
  922. capitalize: false,
  923. })}
  924. </ExternalLink>
  925. ),
  926. attachment: (
  927. <ExternalLink
  928. key="attachment"
  929. href={getDocsLinkForEventType(DataCategoryExact.ATTACHMENT)}
  930. onClick={onClick}
  931. >
  932. {getSingularCategoryName({
  933. plan,
  934. category: DataCategory.ATTACHMENTS,
  935. capitalize: false,
  936. })}
  937. </ExternalLink>
  938. ),
  939. monitorSeat: (
  940. <ExternalLink
  941. key="monitor-seats"
  942. href={getDocsLinkForEventType(DataCategoryExact.MONITOR_SEAT)}
  943. onClick={onClick}
  944. >
  945. {getSingularCategoryName({
  946. plan,
  947. category: DataCategory.MONITOR_SEATS,
  948. capitalize: false,
  949. })}
  950. </ExternalLink>
  951. ),
  952. span: (
  953. <ExternalLink
  954. key="spans"
  955. href={getDocsLinkForEventType(DataCategoryExact.SPAN)}
  956. onClick={onClick}
  957. >
  958. {getSingularCategoryName({
  959. plan,
  960. category: DataCategory.SPANS,
  961. capitalize: false,
  962. })}
  963. </ExternalLink>
  964. ),
  965. uptime: (
  966. <ExternalLink
  967. key="uptime"
  968. href={getDocsLinkForEventType(DataCategoryExact.UPTIME)}
  969. onClick={onClick}
  970. >
  971. {getSingularCategoryName({
  972. plan,
  973. category: DataCategory.UPTIME,
  974. capitalize: false,
  975. })}
  976. </ExternalLink>
  977. ),
  978. // TODO: Uncomment when we have a continuous profile doc link
  979. // profile: (
  980. // <ExternalLink
  981. // key="profiles"
  982. // href={getDocsLinkForEventType(DataCategoryExact.PROFILE)}
  983. // onClick={onClick}
  984. // >
  985. // {getSingularCategoryName({
  986. // plan,
  987. // category: DataCategory.PROFILES,
  988. // capitalize: false,
  989. // })}
  990. // </ExternalLink>
  991. // ),
  992. }[eventType]!;
  993. };
  994. let strictlyCronsOverage = false;
  995. if (isWarning) {
  996. eventTypes = Object.entries(this.overageWarningActive)
  997. .filter(
  998. ([key, value]) =>
  999. value &&
  1000. getActiveProductTrial(
  1001. subscription.productTrials ?? null,
  1002. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  1003. DATA_CATEGORY_INFO[key].plural
  1004. ) === null
  1005. )
  1006. .map(([key, _]) => key as EventType);
  1007. // Make an exception for when only crons has an overage to disable the See Usage button
  1008. strictlyCronsOverage = eventTypes.length === 1 && eventTypes[0] === 'monitorSeat';
  1009. overquotaPrompt = tct(
  1010. 'You are about to exceed your [eventTypes] limit and we will drop any excess events.',
  1011. {
  1012. eventTypes: (
  1013. <b>
  1014. <Oxfordize>{eventTypes.map(eventTypeToElement)}</Oxfordize>
  1015. </b>
  1016. ),
  1017. }
  1018. );
  1019. } else {
  1020. eventTypes = Object.entries(this.overageAlertActive)
  1021. .filter(
  1022. ([key, value]) =>
  1023. value &&
  1024. getActiveProductTrial(
  1025. subscription.productTrials ?? null,
  1026. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  1027. DATA_CATEGORY_INFO[key].plural
  1028. ) === null
  1029. )
  1030. .map(([key, _]) => key as EventType);
  1031. // Make an exception for when only crons has an overage to change the language to be more fitting and hide See Usage
  1032. if (
  1033. eventTypes.length === 1 &&
  1034. (eventTypes[0] === 'monitorSeat' || eventTypes[0] === 'uptime')
  1035. ) {
  1036. overquotaPrompt = tct(
  1037. `We can't enable additional [monitorTitle] because you don't have a sufficient [budgetType] budget.`,
  1038. {
  1039. monitorTitle:
  1040. eventTypes[0] === 'monitorSeat' ? 'Cron Monitors' : 'Uptime Monitors',
  1041. budgetType:
  1042. subscription.planTier === PlanTier.AM3 ? 'pay-as-you-go' : 'on-demand',
  1043. }
  1044. );
  1045. } else {
  1046. overquotaPrompt = tct(
  1047. 'You have exceeded your [eventTypes] limit. We are dropping any excess events until [periodEnd].',
  1048. {
  1049. eventTypes: (
  1050. <b>
  1051. <Oxfordize>{eventTypes.map(eventTypeToElement)}</Oxfordize>
  1052. </b>
  1053. ),
  1054. periodEnd: moment(subscription.onDemandPeriodEnd).add(1, 'days').format('ll'),
  1055. }
  1056. );
  1057. }
  1058. }
  1059. if (eventTypes.length === 0) {
  1060. return null;
  1061. }
  1062. return (
  1063. <Alert
  1064. system
  1065. type={isWarning ? 'muted' : 'warning'}
  1066. showIcon
  1067. data-test-id={'overage-banner-' + eventTypes.join('-')}
  1068. trailingItems={
  1069. <ButtonBar gap={1}>
  1070. {!strictlyCronsOverage && (
  1071. <LinkButton
  1072. size="xs"
  1073. to={`/organizations/${organization.slug}/stats/?dataCategory=${eventTypes[0]}s&pageStart=${subscription.onDemandPeriodStart}&pageEnd=${subscription.onDemandPeriodEnd}&pageUtc=true`}
  1074. onClick={() => {
  1075. trackGetsentryAnalytics('quota_alert.clicked_see_usage', {
  1076. organization,
  1077. subscription,
  1078. event_types: eventTypes.sort().join(','),
  1079. is_warning: isWarning,
  1080. });
  1081. }}
  1082. >
  1083. {t('See Usage')}
  1084. </LinkButton>
  1085. )}
  1086. {this.renderOverageAlertPrimaryCTA(eventTypes, isWarning)}
  1087. <Button
  1088. icon={<IconClose size="sm" />}
  1089. data-test-id="btn-overage-notification-snooze"
  1090. onClick={() => {
  1091. trackGetsentryAnalytics('quota_alert.clicked_snooze', {
  1092. organization,
  1093. subscription,
  1094. event_types: eventTypes.sort().join(','),
  1095. is_warning: isWarning,
  1096. });
  1097. this.handleOverageSnooze(eventTypes, isWarning);
  1098. }}
  1099. size="zero"
  1100. borderless
  1101. title={t('Dismiss this period')}
  1102. aria-label={t('Dismiss this period')}
  1103. />
  1104. </ButtonBar>
  1105. }
  1106. >
  1107. {overquotaPrompt}
  1108. </Alert>
  1109. );
  1110. }
  1111. handleSnoozeMemberDeactivatedAlert = () => {
  1112. const {api, organization, subscription} = this.props;
  1113. promptsUpdate(api, {
  1114. organization,
  1115. feature: 'deactivated_member_alert',
  1116. status: 'snoozed',
  1117. });
  1118. this.setState({deactivatedMemberDismissed: true});
  1119. trackGetsentryAnalytics('deactivated_member_alert.snoozed', {
  1120. organization,
  1121. subscription,
  1122. });
  1123. };
  1124. handleUpgradeLinkClick = () => {
  1125. const {organization, subscription} = this.props;
  1126. trackGetsentryAnalytics('deactivated_member_alert.upgrade_link_clicked', {
  1127. organization,
  1128. subscription,
  1129. });
  1130. };
  1131. PATHS_FOR_PRODUCT_TRIALS = {
  1132. '/issues/': {
  1133. product: DataCategory.ERRORS,
  1134. categories: [DataCategory.ERRORS],
  1135. },
  1136. '/performance/': {
  1137. product: DataCategory.TRANSACTIONS,
  1138. categories: [DataCategory.TRANSACTIONS],
  1139. },
  1140. '/performance/database/': {
  1141. product: DataCategory.TRANSACTIONS,
  1142. categories: [DataCategory.TRANSACTIONS],
  1143. },
  1144. '/replays/': {
  1145. product: DataCategory.REPLAYS,
  1146. categories: [DataCategory.REPLAYS],
  1147. },
  1148. '/profiling/': {
  1149. product: DataCategory.PROFILES,
  1150. categories: [DataCategory.PROFILES, DataCategory.TRANSACTIONS],
  1151. },
  1152. '/insights/backend/crons/': {
  1153. product: DataCategory.MONITOR_SEATS,
  1154. categories: [DataCategory.MONITOR_SEATS],
  1155. },
  1156. '/insights/backend/uptime/': {
  1157. product: DataCategory.UPTIME,
  1158. categories: [DataCategory.UPTIME],
  1159. },
  1160. };
  1161. renderProductTrialAlerts() {
  1162. const {subscription, organization, api} = this.props;
  1163. if (subscription.planTier === PlanTier.AM3) {
  1164. this.PATHS_FOR_PRODUCT_TRIALS['/performance/'] = {
  1165. product: DataCategory.SPANS,
  1166. categories: [DataCategory.SPANS],
  1167. };
  1168. this.PATHS_FOR_PRODUCT_TRIALS['/performance/database/'] = {
  1169. product: DataCategory.SPANS,
  1170. categories: [DataCategory.SPANS],
  1171. };
  1172. }
  1173. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  1174. const productPath = this.PATHS_FOR_PRODUCT_TRIALS[window.location.pathname] || null;
  1175. if (!productPath) {
  1176. return null;
  1177. }
  1178. return productPath.categories
  1179. .map((category: DataCategory) => {
  1180. const categorySnakeCase = category.replace(/[A-Z]/g, c => `_${c.toLowerCase()}`);
  1181. const categorySnakeCaseSingular = categorySnakeCase.substring(
  1182. 0,
  1183. categorySnakeCase.length - 1
  1184. );
  1185. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  1186. const isDismissed = this.state.productTrialDismissed[categorySnakeCaseSingular];
  1187. const trial = getProductTrial(subscription.productTrials ?? null, category);
  1188. return trial && !isDismissed ? (
  1189. <ProductTrialAlert
  1190. key={`${category}-product-trial-alert`}
  1191. trial={trial}
  1192. subscription={subscription}
  1193. organization={organization}
  1194. product={productPath.product}
  1195. api={api}
  1196. onDismiss={() => {
  1197. promptsUpdate(api, {
  1198. organization,
  1199. feature: `${categorySnakeCase}_product_trial_alert`,
  1200. status: 'snoozed',
  1201. });
  1202. this.setState({
  1203. productTrialDismissed: {
  1204. ...this.state.productTrialDismissed,
  1205. [categorySnakeCaseSingular]: true,
  1206. },
  1207. });
  1208. }}
  1209. />
  1210. ) : null;
  1211. })
  1212. .filter((node: any) => node);
  1213. }
  1214. render() {
  1215. const {organization, subscription} = this.props;
  1216. const {deactivatedMemberDismissed} = this.state;
  1217. if (!subscription) {
  1218. return null;
  1219. }
  1220. /**
  1221. * Alert priority:
  1222. * 1. Past due alert
  1223. * 2. Overage alerts
  1224. * 3. Member disabled alerts
  1225. */
  1226. // TODO: Clean up render function
  1227. if (subscription.isPastDue && subscription.canSelfServe) {
  1228. const billingPermissions = this.hasBillingPerms;
  1229. const billingUrl = normalizeUrl(
  1230. `/settings/${organization.slug}/billing/details/?referrer=banner-billing-failure`
  1231. );
  1232. const membersPageUrl = makeLinkToOwnersAndBillingMembers(
  1233. organization,
  1234. 'past_due_banner-alert'
  1235. );
  1236. const addButtonAnalytics = () => {
  1237. trackGetsentryAnalytics('billing_failure.button_clicked', {
  1238. organization,
  1239. has_permissions: billingPermissions,
  1240. referrer: 'banner-billing-failure',
  1241. });
  1242. };
  1243. return (
  1244. <Alert.Container>
  1245. <BannerAlert
  1246. system
  1247. data-test-id="banner-alert-past-due"
  1248. type="muted"
  1249. trailingItems={<Badge type="warning">{t('Action Required')}</Badge>}
  1250. >
  1251. {billingPermissions
  1252. ? tct(
  1253. 'There was an issue with your payment. [updateUrl:Update your payment information] to ensure uninterrupted access to Sentry.',
  1254. {
  1255. updateUrl: (
  1256. <Button
  1257. to={billingUrl}
  1258. size="xs"
  1259. priority="default"
  1260. aria-label={t('Update payment information')}
  1261. onClick={addButtonAnalytics}
  1262. />
  1263. ),
  1264. }
  1265. )
  1266. : tct(
  1267. 'There was an issue with your payment. Please have the [updateUrl: Org Owner or Billing Member] update your payment information to ensure continued access to Sentry.',
  1268. {
  1269. updateUrl: (
  1270. <Button
  1271. to={membersPageUrl}
  1272. size="xs"
  1273. priority="default"
  1274. aria-label={t('Org Owner or Billing Member')}
  1275. onClick={addButtonAnalytics}
  1276. />
  1277. ),
  1278. }
  1279. )}
  1280. </BannerAlert>
  1281. </Alert.Container>
  1282. );
  1283. }
  1284. const productTrialAlerts = this.renderProductTrialAlerts();
  1285. const overageAlertType = this.overageAlertType;
  1286. if (overageAlertType !== null) {
  1287. return (
  1288. <React.Fragment>
  1289. {productTrialAlerts && productTrialAlerts.length > 0 && productTrialAlerts}
  1290. {this.renderOverageAlert(overageAlertType === 'warning')}
  1291. </React.Fragment>
  1292. );
  1293. }
  1294. const {membersDeactivatedFromLimit} = subscription;
  1295. const isOverMemberLimit = membersDeactivatedFromLimit > 0;
  1296. // if there are deactivated members, than anyone who doesn't have org:billing will be
  1297. // prevented from accessing this view anyways cause they will be deactivated
  1298. if (isOverMemberLimit && !deactivatedMemberDismissed && this.hasBillingPerms) {
  1299. const checkoutUrl = `/settings/${organization.slug}/billing/checkout/?referrer=deactivated_member_header`;
  1300. const wrappedNumber = <strong>{membersDeactivatedFromLimit}</strong>;
  1301. // only disabling members if the plan allows exactly one member
  1302. return (
  1303. <React.Fragment>
  1304. {productTrialAlerts && productTrialAlerts.length > 0 && productTrialAlerts}
  1305. <Alert.Container>
  1306. <BannerAlert
  1307. system
  1308. type="muted"
  1309. trailingItems={
  1310. <ButtonBar gap={1}>
  1311. <Button
  1312. to={checkoutUrl}
  1313. onClick={this.handleUpgradeLinkClick}
  1314. size="xs"
  1315. priority="primary"
  1316. >
  1317. {t('Upgrade')}
  1318. </Button>
  1319. <Button
  1320. onClick={this.handleSnoozeMemberDeactivatedAlert}
  1321. size="xs"
  1322. priority="default"
  1323. title={t(
  1324. 'You can also resolve this warning by removing the deactivated members from your organization'
  1325. )}
  1326. >
  1327. {t('Snooze')}
  1328. </Button>
  1329. </ButtonBar>
  1330. }
  1331. >
  1332. {tct(
  1333. `[firstSentence] [middleSentence] Upgrade your plan to increase your limit.`,
  1334. {
  1335. firstSentence:
  1336. subscription.totalLicenses === 1
  1337. ? t('Your plan is limited to one user.')
  1338. : tct('Your plan is limited to [totalLicenses] users.', {
  1339. totalLicenses: subscription.totalLicenses,
  1340. }),
  1341. middleSentence:
  1342. membersDeactivatedFromLimit === 1
  1343. ? tct('[wrappedNumber] member has been deactivated.', {
  1344. wrappedNumber,
  1345. })
  1346. : tct('[wrappedNumber] members have been deactivated.', {
  1347. wrappedNumber,
  1348. }),
  1349. }
  1350. )}
  1351. </BannerAlert>
  1352. </Alert.Container>
  1353. </React.Fragment>
  1354. );
  1355. }
  1356. return productTrialAlerts ?? null;
  1357. }
  1358. }
  1359. export default withPromotions(withApi(withSubscription(GSBanner, {noLoader: true})));
  1360. // XXX: We have no alert types with this styling, but for now we would like for
  1361. // it to be differentiated.
  1362. const BannerAlert = styled(Alert)`
  1363. color: ${p => p.theme.headerBackground};
  1364. background-color: ${p => p.theme.bannerBackground};
  1365. border: none;
  1366. `;