subscriptionUpsellBanner.tsx 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. import styled from '@emotion/styled';
  2. import businessUpgrade from 'getsentry-images/product_trial/business-upgrade-notrial.svg';
  3. import businessTrial from 'getsentry-images/product_trial/try-sentry-business-present.svg';
  4. import {usePrompt} from 'sentry/actionCreators/prompts';
  5. import {Button} from 'sentry/components/button';
  6. import {IconClose} from 'sentry/icons';
  7. import {t, tct} from 'sentry/locale';
  8. import {space} from 'sentry/styles/space';
  9. import type {Organization} from 'sentry/types/organization';
  10. import {openUpsellModal} from 'getsentry/actionCreators/modal';
  11. import UpgradeOrTrialButton from 'getsentry/components/upgradeOrTrialButton';
  12. import {usePlanMigrations} from 'getsentry/hooks/usePlanMigrations';
  13. import type {Subscription} from 'getsentry/types';
  14. import {hasPerformance, isBizPlanFamily} from 'getsentry/utils/billing';
  15. import TrialBadge from 'getsentry/views/subscriptionPage/trial/badge';
  16. const getSubscriptionBannerText = (
  17. organization: Organization,
  18. subscription: Subscription
  19. ): [headerText: React.ReactNode, subText: React.ReactNode] => {
  20. const hasBillingPerms = organization.access?.includes('org:billing');
  21. const featuresName = hasPerformance(subscription.planDetails)
  22. ? 'Business plan'
  23. : 'Performance';
  24. // upsell a trial if available
  25. if (subscription.canTrial) {
  26. if (hasBillingPerms) {
  27. return [
  28. hasPerformance(subscription.planDetails)
  29. ? t('Try Sentry Business for Free')
  30. : t('Try Performance for Free'),
  31. tct(
  32. `Activate your trial to take advantage of Sentry's [featuresName] features.`,
  33. {featuresName}
  34. ),
  35. ];
  36. }
  37. return [
  38. hasPerformance(subscription.planDetails)
  39. ? t('Request a Free Sentry Business Trial')
  40. : t('Request a Free Sentry Performance Trial'),
  41. tct(
  42. '[italicized] your Organization’s owner to start a [featuresName] trial (See what I did there?).',
  43. {
  44. italicized: <i>{t('Bug')}</i>,
  45. featuresName,
  46. }
  47. ),
  48. ];
  49. }
  50. // if on free plan, we need to get to a paid plan
  51. return hasBillingPerms
  52. ? [
  53. t('Upgrade to Business'),
  54. t(
  55. 'Advanced integrations, deep insights, custom dashboards, and more. Upgrade to Sentry’s Business plan today.'
  56. ),
  57. ]
  58. : [
  59. t('Request an Upgrade to Business'),
  60. tct(
  61. '[italicized] your Organization’s owner to upgrade Sentry (See what I did there?).',
  62. {
  63. italicized: <i>{t('Bug')}</i>,
  64. }
  65. ),
  66. ];
  67. };
  68. function useIsSubscriptionUpsellHidden(
  69. subscription: Subscription,
  70. organization: Organization
  71. ): boolean {
  72. const {planMigrations, isLoading} = usePlanMigrations();
  73. // Hide while loading
  74. if (isLoading) {
  75. return true;
  76. }
  77. // hide upsell for mmx plans and forced plan migrations
  78. const isLegacyUpsell =
  79. (!hasPerformance(subscription.planDetails) || planMigrations.length > 0) &&
  80. !subscription.canTrial;
  81. // hide upsell for customers on partner plans with flag
  82. const hasPartnerMigrationFeature = organization.features.includes(
  83. 'partner-billing-migration'
  84. );
  85. // exclude current tiers business, non-self serve, current trial orgs, legacy upsells, and orgs with pending business upgrade
  86. if (
  87. !subscription.canSelfServe ||
  88. (hasPerformance(subscription.planDetails) &&
  89. isBizPlanFamily(subscription.planDetails)) ||
  90. subscription.isTrial ||
  91. isLegacyUpsell ||
  92. hasPartnerMigrationFeature ||
  93. isBizPlanFamily(subscription.pendingChanges?.planDetails)
  94. ) {
  95. return true;
  96. }
  97. return false;
  98. }
  99. const BANNER_PROMPT_KEY = 'subscription_try_business_banner';
  100. interface SubscriptionUpsellBannerProps {
  101. organization: Organization;
  102. subscription: Subscription;
  103. }
  104. export function SubscriptionUpsellBanner({
  105. organization,
  106. subscription,
  107. }: SubscriptionUpsellBannerProps) {
  108. const isHidden = useIsSubscriptionUpsellHidden(subscription, organization);
  109. const {isLoading, isError, isPromptDismissed, dismissPrompt} = usePrompt({
  110. feature: BANNER_PROMPT_KEY,
  111. organization,
  112. options: {enabled: !isHidden},
  113. });
  114. if (isHidden || isPromptDismissed || isLoading || isError) {
  115. return null;
  116. }
  117. const [title, description] = getSubscriptionBannerText(organization, subscription);
  118. return (
  119. <BusinessTrialBannerWrapper>
  120. <div>
  121. <IntegationBannerTitle>
  122. {title}
  123. {subscription.canTrial && (
  124. <TrialBadge subscription={subscription} organization={organization} />
  125. )}
  126. </IntegationBannerTitle>
  127. <IntegationBannerDescription>
  128. {description}{' '}
  129. <Button
  130. size="zero"
  131. priority="link"
  132. onClick={() =>
  133. openUpsellModal({organization, source: 'subscription_overview'})
  134. }
  135. >
  136. {t('Learn More')}
  137. </Button>
  138. </IntegationBannerDescription>
  139. <UpgradeOrTrialButton
  140. subscription={subscription}
  141. organization={organization}
  142. source="subscription-overview"
  143. size="sm"
  144. >
  145. {({hasBillingAccess, action}) => {
  146. // overide the CTA for starting a trial
  147. if (hasBillingAccess && action === 'trial') {
  148. return t('Start Trial');
  149. }
  150. return null;
  151. }}
  152. </UpgradeOrTrialButton>
  153. </div>
  154. <BannerImage src={subscription.canTrial ? businessTrial : businessUpgrade} />
  155. <CloseBannerButton
  156. borderless
  157. priority="link"
  158. aria-label={t('Dismiss')}
  159. icon={<IconClose color="subText" />}
  160. size="xs"
  161. onClick={dismissPrompt}
  162. />
  163. </BusinessTrialBannerWrapper>
  164. );
  165. }
  166. const BusinessTrialBannerWrapper = styled('div')`
  167. position: relative;
  168. border: 1px solid ${p => p.theme.border};
  169. border-radius: ${p => p.theme.borderRadius};
  170. padding: ${space(2)};
  171. margin: ${space(1)} 0;
  172. background: linear-gradient(
  173. 90deg,
  174. ${p => p.theme.backgroundSecondary}00 0%,
  175. ${p => p.theme.backgroundSecondary}FF 70%,
  176. ${p => p.theme.backgroundSecondary}FF 100%
  177. );
  178. margin-bottom: 24px;
  179. `;
  180. const IntegationBannerTitle = styled('div')`
  181. display: flex;
  182. align-items: baseline;
  183. gap: ${space(1)};
  184. font-size: ${p => p.theme.fontSizeExtraLarge};
  185. margin-bottom: ${space(1)};
  186. font-weight: 600;
  187. `;
  188. const IntegationBannerDescription = styled('div')`
  189. margin-bottom: ${space(1.5)};
  190. max-width: 440px;
  191. `;
  192. const CloseBannerButton = styled(Button)`
  193. position: absolute;
  194. display: block;
  195. top: ${space(2)};
  196. right: ${space(2)};
  197. color: ${p => p.theme.white};
  198. cursor: pointer;
  199. z-index: 1;
  200. `;
  201. const BannerImage = styled('img')`
  202. position: absolute;
  203. display: none;
  204. bottom: 0px;
  205. right: 4rem;
  206. pointer-events: none;
  207. max-height: 90%;
  208. @media (min-width: ${p => p.theme.breakpoints.xlarge}) {
  209. display: block;
  210. }
  211. `;