upsellProvider.tsx 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. import {useState} from 'react';
  2. import {addErrorMessage} from 'sentry/actionCreators/indicator';
  3. import type {Client} from 'sentry/api';
  4. import {Button} from 'sentry/components/button';
  5. import Confirm from 'sentry/components/confirm';
  6. import {t, tct} from 'sentry/locale';
  7. import type {Organization} from 'sentry/types/organization';
  8. import {browserHistory} from 'sentry/utils/browserHistory';
  9. import normalizeUrl from 'sentry/utils/url/normalizeUrl';
  10. import withApi from 'sentry/utils/withApi';
  11. import withOrganization from 'sentry/utils/withOrganization';
  12. import {openUpsellModal} from 'getsentry/actionCreators/modal';
  13. import {sendTrialRequest, sendUpgradeRequest} from 'getsentry/actionCreators/upsell';
  14. import TrialStarter from 'getsentry/components/trialStarter';
  15. import withSubscription from 'getsentry/components/withSubscription';
  16. import type {Subscription} from 'getsentry/types';
  17. import {getTrialLength} from 'getsentry/utils/billing';
  18. import trackGetsentryAnalytics, {
  19. type GetsentryEventKey,
  20. } from 'getsentry/utils/trackGetsentryAnalytics';
  21. type ChildRenderProps = {
  22. action:
  23. | 'start_trial'
  24. | 'send_to_checkout'
  25. | 'request_trial'
  26. | 'request_upgrade'
  27. | 'open_upsell_modal';
  28. canTrial: boolean;
  29. defaultButtonText: string;
  30. hasBillingScope: boolean;
  31. onClick: (e?: React.MouseEvent) => void;
  32. };
  33. type Props = {
  34. api: Client;
  35. children: (opts: ChildRenderProps) => React.ReactNode;
  36. organization: Organization;
  37. source: string;
  38. subscription: Subscription;
  39. extraAnalyticsParams?: Record<string, any>;
  40. onTrialStarted?: () => void;
  41. /**
  42. * When this is true, a Confirm modal will be displayed before activating the trial
  43. */
  44. showConfirmation?: boolean;
  45. /**
  46. * if true, non-billing users clicking will trigger trial and plan upgrade requests
  47. */
  48. triggerMemberRequests?: boolean;
  49. upsellDefaultSelection?: string;
  50. };
  51. function LoadingButton(props: {
  52. defaultOnClick: () => void;
  53. startTrial: () => Promise<void>;
  54. }) {
  55. const {startTrial, defaultOnClick} = props;
  56. const [busy, setBusy] = useState(false);
  57. return (
  58. <Button
  59. autoFocus
  60. priority="primary"
  61. busy={busy}
  62. onClick={async () => {
  63. setBusy(true);
  64. await startTrial();
  65. defaultOnClick();
  66. }}
  67. >
  68. {t('Start Trial')}
  69. </Button>
  70. );
  71. }
  72. function UpsellProvider({
  73. api,
  74. onTrialStarted,
  75. organization,
  76. subscription,
  77. source,
  78. extraAnalyticsParams,
  79. triggerMemberRequests,
  80. showConfirmation,
  81. upsellDefaultSelection,
  82. children,
  83. }: Props) {
  84. // if the org or subscription isn't loaded yet, don't render anything
  85. if (!organization || !subscription) {
  86. return null;
  87. }
  88. const hasBillingScope = organization.access?.includes('org:billing');
  89. // don't render any request trial/upgrade CTAs for non-self serve customers
  90. if (!hasBillingScope && !subscription.canSelfServe && triggerMemberRequests) {
  91. return null;
  92. }
  93. const canTrial = subscription.canTrial && !subscription.isTrial;
  94. const handleRequest = () => {
  95. const args = {
  96. api,
  97. organization,
  98. };
  99. if (canTrial) {
  100. return sendTrialRequest(args);
  101. }
  102. return sendUpgradeRequest(args);
  103. };
  104. let defaultButtonText: string;
  105. if (hasBillingScope || !triggerMemberRequests) {
  106. defaultButtonText = canTrial ? t('Start Trial') : t('Upgrade Plan');
  107. } else {
  108. defaultButtonText = canTrial ? t('Request Trial') : t('Request Upgrade');
  109. }
  110. const getAction = () => {
  111. if (hasBillingScope) {
  112. return canTrial ? 'start_trial' : 'send_to_checkout';
  113. }
  114. if (triggerMemberRequests) {
  115. return canTrial ? 'request_trial' : 'request_upgrade';
  116. }
  117. return 'open_upsell_modal';
  118. };
  119. showConfirmation = showConfirmation && hasBillingScope && canTrial;
  120. const collectAnalytics = (eventKey: GetsentryEventKey) => {
  121. trackGetsentryAnalytics(eventKey, {
  122. source,
  123. can_trial: canTrial,
  124. has_billing_scope: hasBillingScope,
  125. showed_confirmation: !!showConfirmation,
  126. action: getAction(),
  127. organization,
  128. subscription,
  129. ...extraAnalyticsParams,
  130. });
  131. };
  132. const trialStartProps = {
  133. organization,
  134. source,
  135. onTrialFailed: () => {
  136. addErrorMessage(t('Error starting trial. Please try again.'));
  137. },
  138. onTrialStarted,
  139. };
  140. if (showConfirmation) {
  141. const confirmContent = () => {
  142. const trialLength = getTrialLength(organization);
  143. return (
  144. <div data-test-id="confirm-content">
  145. {tct(
  146. `Your organization is about to start a [trialLength]-day free trial. Click confirm to start your trial.`,
  147. {
  148. trialLength,
  149. }
  150. )}
  151. </div>
  152. );
  153. };
  154. return (
  155. <Confirm
  156. renderMessage={confirmContent}
  157. onConfirm={() => collectAnalytics('growth.upsell_feature.confirmed')}
  158. onCancel={() => collectAnalytics('growth.upsell_feature.cancelled')}
  159. renderConfirmButton={({defaultOnClick}) => (
  160. <TrialStarter {...trialStartProps}>
  161. {({startTrial}) => (
  162. <LoadingButton startTrial={startTrial} defaultOnClick={defaultOnClick} />
  163. )}
  164. </TrialStarter>
  165. )}
  166. header={<h4>{t('Your trial is about to start')}</h4>}
  167. >
  168. {({open}) =>
  169. children({
  170. canTrial,
  171. hasBillingScope,
  172. defaultButtonText,
  173. action: getAction(),
  174. onClick: () => {
  175. // When the user clicks, open the modal
  176. collectAnalytics('growth.upsell_feature.clicked');
  177. open();
  178. },
  179. })
  180. }
  181. </Confirm>
  182. );
  183. }
  184. return (
  185. <TrialStarter {...trialStartProps}>
  186. {({startTrial}) =>
  187. children({
  188. canTrial,
  189. hasBillingScope,
  190. defaultButtonText,
  191. action: getAction(),
  192. onClick: e => {
  193. e?.preventDefault();
  194. // Direct start the trial.
  195. collectAnalytics('growth.upsell_feature.clicked');
  196. if (hasBillingScope) {
  197. if (canTrial) {
  198. startTrial();
  199. } else {
  200. // for self-serve can send them to checkout
  201. const baseUrl = subscription.canSelfServe
  202. ? `/settings/${organization.slug}/billing/checkout/`
  203. : `/settings/${organization.slug}/billing/overview/`;
  204. browserHistory.push(`${normalizeUrl(baseUrl)}?referrer=upsell-${source}`);
  205. }
  206. } else {
  207. if (triggerMemberRequests) {
  208. handleRequest();
  209. } else {
  210. openUpsellModal({
  211. organization,
  212. source,
  213. defaultSelection: upsellDefaultSelection,
  214. });
  215. }
  216. }
  217. },
  218. })
  219. }
  220. </TrialStarter>
  221. );
  222. }
  223. export default withApi(
  224. withOrganization(withSubscription(UpsellProvider, {noLoader: true}))
  225. );