upgradeOrTrialButton.tsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. import {useState} from 'react';
  2. import type {Client} from 'sentry/api';
  3. import type {ButtonProps} from 'sentry/components/button';
  4. import {Button} from 'sentry/components/button';
  5. import {t} from 'sentry/locale';
  6. import type {Organization} from 'sentry/types/organization';
  7. import withApi from 'sentry/utils/withApi';
  8. import {sendTrialRequest, sendUpgradeRequest} from 'getsentry/actionCreators/upsell';
  9. import StartTrialButton from 'getsentry/components/startTrialButton';
  10. import type {Subscription} from 'getsentry/types';
  11. import {getTrialLength} from 'getsentry/utils/billing';
  12. import trackGetsentryAnalytics from 'getsentry/utils/trackGetsentryAnalytics';
  13. type ChildRenderProps = {
  14. action: 'upgrade' | 'trial';
  15. hasBillingAccess: boolean;
  16. };
  17. type ChildRenderFunction = (options: ChildRenderProps) => React.ReactNode;
  18. type Props = {
  19. api: Client;
  20. organization: Organization;
  21. source: string;
  22. subscription: Subscription;
  23. action?: 'upgrade' | 'trial';
  24. children?: React.ReactNode | ChildRenderFunction;
  25. onSuccess?: () => void;
  26. requestData?: Record<string, unknown>;
  27. /**
  28. * Default button priority to use when the button triggers an upgrade
  29. */
  30. trialPriority?: ButtonProps['priority'];
  31. /**
  32. * Default button priority to use when the button will start a trial
  33. */
  34. upgradePriority?: ButtonProps['priority'];
  35. } & Omit<React.ComponentProps<typeof Button>, 'to' | 'onClick' | 'busy' | 'children'>;
  36. /**
  37. * This button has the following modes:
  38. * 1. Start trial (admins with trial available)
  39. * 2. Go to checkout page
  40. * 3. Request trial (if trial available but no permissions)
  41. * 4. Request upgrade
  42. */
  43. function UpgradeOrTrialButton({
  44. source,
  45. organization,
  46. subscription,
  47. onSuccess,
  48. action: _action,
  49. api,
  50. children,
  51. requestData,
  52. upgradePriority = 'primary',
  53. trialPriority = 'primary',
  54. ...props
  55. }: Props) {
  56. const {slug} = organization;
  57. const hasAccess = organization.access.includes('org:billing');
  58. // can override action if we want
  59. const action =
  60. _action ?? (subscription.canTrial && !subscription.isTrial ? 'trial' : 'upgrade');
  61. const childComponent =
  62. typeof children === 'function'
  63. ? children({action, hasBillingAccess: hasAccess})
  64. : children;
  65. // The button color depends on the priority, and that is determined by the action
  66. const buttonPriority: ButtonProps['priority'] =
  67. action === 'trial' ? trialPriority : upgradePriority;
  68. const recordAnalytics = () => {
  69. const getVerb = () => {
  70. if (!hasAccess) {
  71. return 'requested';
  72. }
  73. // need a different verb for upgrades since it's just a link
  74. return action === 'upgrade' ? 'link_clicked' : 'started';
  75. };
  76. trackGetsentryAnalytics('growth.upgrade_or_trial.clicked', {
  77. action: `${action}.${getVerb()}`,
  78. source,
  79. organization,
  80. subscription,
  81. });
  82. };
  83. const handleSuccess = () => {
  84. recordAnalytics();
  85. onSuccess?.();
  86. };
  87. const [busy, setBusy] = useState(false);
  88. const handleRequest = async () => {
  89. setBusy(true);
  90. const args = {
  91. api,
  92. organization,
  93. handleSuccess,
  94. };
  95. if (action === 'trial') {
  96. await sendTrialRequest(args);
  97. } else {
  98. await sendUpgradeRequest(args);
  99. }
  100. setBusy(false);
  101. };
  102. // we don't want non-self serve customers to see request to trial/upgrade CTAs
  103. if (!hasAccess && !subscription.canSelfServe) {
  104. return null;
  105. }
  106. if (action === 'trial') {
  107. if (hasAccess) {
  108. // admin with trial available
  109. return (
  110. <StartTrialButton
  111. organization={organization}
  112. source={source}
  113. onTrialStarted={handleSuccess}
  114. requestData={requestData}
  115. priority={buttonPriority}
  116. {...props}
  117. >
  118. {childComponent || t('Start %s-Day Trial', getTrialLength(organization))}
  119. </StartTrialButton>
  120. );
  121. }
  122. // non-admin who wants to trial
  123. return (
  124. <Button onClick={handleRequest} busy={busy} priority={buttonPriority} {...props}>
  125. {childComponent || t('Request Trial')}
  126. </Button>
  127. );
  128. }
  129. if (hasAccess) {
  130. // send self-serve directly to checkout
  131. const baseUrl = subscription.canSelfServe
  132. ? `/settings/${slug}/billing/checkout/`
  133. : `/settings/${slug}/billing/overview/`;
  134. return (
  135. <Button
  136. onClick={handleSuccess}
  137. to={`${baseUrl}?referrer=upgrade-${source}`}
  138. priority={buttonPriority}
  139. {...props}
  140. >
  141. {childComponent || t('Upgrade now')}
  142. </Button>
  143. );
  144. }
  145. return (
  146. <Button onClick={handleRequest} busy={busy} priority={buttonPriority} {...props}>
  147. {childComponent || t('Request Upgrade')}
  148. </Button>
  149. );
  150. }
  151. export default withApi(UpgradeOrTrialButton);