forcedTrialModal.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. import {Fragment} from 'react';
  2. import {css} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import type {ModalRenderProps} from 'sentry/actionCreators/modal';
  5. import ButtonBar from 'sentry/components/buttonBar';
  6. import {Button} from 'sentry/components/core/button';
  7. import HighlightModalContainer from 'sentry/components/highlightModalContainer';
  8. import LoadingError from 'sentry/components/loadingError';
  9. import LoadingIndicator from 'sentry/components/loadingIndicator';
  10. import {t} from 'sentry/locale';
  11. import {space} from 'sentry/styles/space';
  12. import type {Integration} from 'sentry/types/integrations';
  13. import type {Organization} from 'sentry/types/organization';
  14. import {useApiQuery} from 'sentry/utils/queryClient';
  15. import UpgradeOrTrialButton from 'getsentry/components/upgradeOrTrialButton';
  16. import withSubscription from 'getsentry/components/withSubscription';
  17. import type {Subscription} from 'getsentry/types';
  18. import {getTrialDaysLeft, getTrialLength} from 'getsentry/utils/billing';
  19. const INTEGRATIONS_TO_CHECK = ['slack'];
  20. interface ForcedTrialModalProps extends Pick<ModalRenderProps, 'closeModal'> {
  21. organization: Organization;
  22. subscription: Subscription;
  23. }
  24. function ForcedTrialModal(props: ForcedTrialModalProps) {
  25. const {organization, subscription, closeModal} = props;
  26. const hasBillingScope = organization.access.includes('org:billing');
  27. const {
  28. data: configurations,
  29. isPending,
  30. isError,
  31. } = useApiQuery<Integration[]>(
  32. [
  33. `/organizations/${organization.slug}/integrations/`,
  34. {
  35. query: {
  36. includeConfig: 0,
  37. },
  38. },
  39. ],
  40. {
  41. staleTime: 120000,
  42. }
  43. );
  44. if (isPending) {
  45. return <LoadingIndicator />;
  46. }
  47. if (isError) {
  48. return <LoadingError />;
  49. }
  50. const daysLeft = getTrialDaysLeft(subscription);
  51. if (daysLeft < 0) {
  52. return null;
  53. }
  54. const disallowedIntegration = (configurations || []).find(config =>
  55. INTEGRATIONS_TO_CHECK.includes(config.provider.slug)
  56. );
  57. const mainHeader = disallowedIntegration
  58. ? t(
  59. 'Your %s integration will stop working in %s days',
  60. disallowedIntegration.provider.name,
  61. daysLeft
  62. )
  63. : hasBillingScope
  64. ? t('Members may lose access to Sentry in %s days', daysLeft)
  65. : t('You may lose access to Sentry in %s days', daysLeft);
  66. const firstParagraph = disallowedIntegration
  67. ? t(
  68. `Your %s organization is on the Developer plan and does not support the %s integration.`,
  69. organization.slug,
  70. disallowedIntegration.provider.name
  71. )
  72. : t(
  73. 'Your %s organization is on the Developer plan and does not allow for multiple members.',
  74. organization.slug
  75. );
  76. const secondParagraph = disallowedIntegration ? (
  77. <Fragment>
  78. {t(
  79. 'In %s days, your %s integration will be disabled.',
  80. daysLeft,
  81. disallowedIntegration.provider.name
  82. )}{' '}
  83. {t(
  84. 'Upgrade to our Team or Business plan so you can keep using %s.',
  85. disallowedIntegration.provider.name
  86. )}
  87. </Fragment>
  88. ) : (
  89. <Fragment>
  90. {t('In %s days, your organization will be limited to 1 user.', daysLeft)}{' '}
  91. {hasBillingScope
  92. ? t('Upgrade to our Team or Business plan so your members can retain access.')
  93. : t(
  94. 'Ask your organization owner to upgrade to our Team or Business plan to retain access.'
  95. )}
  96. </Fragment>
  97. );
  98. // TODO: add explicit check that org has additional members if no restricted integrations
  99. return (
  100. <HighlightModalContainer>
  101. <div>
  102. <TrialCheckInfo>
  103. <Subheader>
  104. {t('%s-day Business Trial', getTrialLength(organization))}
  105. </Subheader>
  106. <h2>{mainHeader}</h2>
  107. <p>{firstParagraph}</p>
  108. <br />
  109. <p>{secondParagraph}</p>
  110. </TrialCheckInfo>
  111. <StyledButtonBar gap={2}>
  112. <UpgradeOrTrialButton
  113. source="force_trial_modal"
  114. action="upgrade"
  115. subscription={subscription}
  116. organization={organization}
  117. onSuccess={closeModal}
  118. >
  119. {hasBillingScope ? t('Upgrade') : t('Request Upgrade')}
  120. </UpgradeOrTrialButton>
  121. <Button data-test-id="maybe-later" priority="default" onClick={closeModal}>
  122. {t('Continue with Trial')}
  123. </Button>
  124. </StyledButtonBar>
  125. </div>
  126. </HighlightModalContainer>
  127. );
  128. }
  129. const TrialCheckInfo = styled('div')`
  130. padding: ${space(3)} 0;
  131. p {
  132. font-size: ${p => p.theme.fontSizeMedium};
  133. margin: 0;
  134. }
  135. h2 {
  136. font-size: 1.5em;
  137. }
  138. `;
  139. export const modalCss = css`
  140. width: 100%;
  141. max-width: 730px;
  142. [role='document'] {
  143. position: relative;
  144. padding: 70px 80px;
  145. overflow: hidden;
  146. }
  147. `;
  148. const Subheader = styled('h4')`
  149. margin-bottom: ${space(2)};
  150. text-transform: uppercase;
  151. font-weight: bold;
  152. color: ${p => p.theme.purple300};
  153. font-size: ${p => p.theme.fontSizeExtraSmall};
  154. `;
  155. const StyledButtonBar = styled(ButtonBar)`
  156. margin-top: ${space(2)};
  157. max-width: fit-content;
  158. `;
  159. export default withSubscription(ForcedTrialModal);