modal.tsx 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. import type {ComponentType} from 'react';
  2. import styled from '@emotion/styled';
  3. import type {Location} from 'history';
  4. import {openModal} from 'sentry/actionCreators/modal';
  5. import {promptsUpdate} from 'sentry/actionCreators/prompts';
  6. import {Client} from 'sentry/api';
  7. import {openConfirmModal} from 'sentry/components/confirm';
  8. import {t} from 'sentry/locale';
  9. import type {Organization} from 'sentry/types/organization';
  10. import type {PromotionModalBodyProps} from 'getsentry/components/promotionModal';
  11. import type {Reservations} from 'getsentry/components/upgradeNowModal/types';
  12. import SubscriptionStore from 'getsentry/stores/subscriptionStore';
  13. import type {
  14. Invoice,
  15. Plan,
  16. PreviewData,
  17. Promotion,
  18. PromotionClaimed,
  19. Subscription,
  20. } from 'getsentry/types';
  21. import type {AM2UpdateSurfaces} from 'getsentry/utils/trackGetsentryAnalytics';
  22. type UpsellModalOptions = {
  23. organization: Organization;
  24. source: string;
  25. defaultSelection?: string;
  26. };
  27. export async function openUpsellModal(options: UpsellModalOptions) {
  28. const {default: Modal, modalCss} = await import('getsentry/components/upsellModal');
  29. openModal(deps => <Modal {...deps} {...options} />, {modalCss});
  30. }
  31. type TrialModalProps = {
  32. organization: Organization;
  33. };
  34. type PartnerPlanModalProps = {
  35. organization: Organization;
  36. subscription: Subscription;
  37. };
  38. function genTrialModalOnClose(
  39. options: TrialModalProps,
  40. type: 'trialEnd' | 'forcedTrial'
  41. ) {
  42. let feature: string, subField: string;
  43. switch (type) {
  44. case 'trialEnd':
  45. feature = 'trial_ended_notice';
  46. subField = 'hasDismissedTrialEndingNotice';
  47. break;
  48. case 'forcedTrial':
  49. feature = 'forced_trial_notice';
  50. subField = 'hasDismissedForcedTrialNotice';
  51. break;
  52. default:
  53. throw new Error('Unexpected type');
  54. }
  55. const api = new Client();
  56. const promptParams = {
  57. organization: options.organization,
  58. feature,
  59. status: 'dismissed',
  60. } as const;
  61. const subUpdate = {
  62. [subField]: true,
  63. } as const;
  64. // Handle marking the feature prompt as seen when the modal is
  65. // closed
  66. return () => {
  67. promptsUpdate(api, promptParams);
  68. SubscriptionStore.set(options.organization.slug, subUpdate);
  69. };
  70. }
  71. export async function openTrialEndingModal(options: TrialModalProps) {
  72. const {default: Modal, modalCss} = await import(
  73. 'getsentry/components/trialEndingModal'
  74. );
  75. const onClose = genTrialModalOnClose(options, 'trialEnd');
  76. openModal(deps => <Modal {...deps} {...options} />, {modalCss, onClose});
  77. }
  78. export async function openForcedTrialModal(options: TrialModalProps) {
  79. const {default: Modal, modalCss} = await import(
  80. 'getsentry/components/forcedTrialModal'
  81. );
  82. const onClose = genTrialModalOnClose(options, 'forcedTrial');
  83. openModal(deps => <Modal {...deps} {...options} />, {
  84. modalCss,
  85. onClose,
  86. });
  87. }
  88. export async function openPartnerPlanEndingModal(options: PartnerPlanModalProps) {
  89. const {default: Modal, modalCss} = await import(
  90. 'getsentry/components/partnerPlanEndingModal'
  91. );
  92. const api = new Client();
  93. const promptParams = {
  94. organization: options.organization,
  95. feature: 'partner_plan_ending_modal',
  96. status: 'dismissed',
  97. } as const;
  98. const onClose = () => {
  99. promptsUpdate(api, promptParams);
  100. };
  101. openModal(deps => <Modal {...deps} {...options} />, {modalCss, onClose});
  102. }
  103. type EditCreditCardOptions = {
  104. onSuccess: (data: Subscription) => void;
  105. organization: Organization;
  106. location?: Location;
  107. };
  108. export async function openEditCreditCard(options: EditCreditCardOptions) {
  109. const {default: Modal} = await import('getsentry/components/creditCardEditModal');
  110. openModal(deps => <Modal {...deps} {...options} />);
  111. }
  112. type OpenInvoicePaymentOptions = {
  113. invoice: Invoice;
  114. organization: Organization;
  115. reloadInvoice: () => void;
  116. };
  117. export async function openInvoicePaymentModal(options: OpenInvoicePaymentOptions) {
  118. const {default: Modal} = await import('getsentry/views/invoiceDetails/paymentForm');
  119. openModal(deps => <Modal {...deps} {...options} />);
  120. }
  121. type UpsellModalProps = {
  122. organization: Organization;
  123. plan: Plan;
  124. previewData: PreviewData;
  125. reservations: Reservations;
  126. subscription: Subscription;
  127. surface: AM2UpdateSurfaces;
  128. isActionDisabled?: boolean;
  129. onComplete?: () => void;
  130. };
  131. export async function openAM2UpsellModal(options: UpsellModalProps) {
  132. const {default: Modal, modalCss} = await import(
  133. 'getsentry/components/upgradeNowModal/index'
  134. );
  135. openModal(deps => <Modal {...deps} {...options} />, {modalCss});
  136. }
  137. export type UpsellModalSamePriceProps = {
  138. organization: Organization;
  139. plan: Plan;
  140. previewData: PreviewData;
  141. reservations: Reservations;
  142. subscription: Subscription;
  143. surface: AM2UpdateSurfaces;
  144. onComplete?: () => void;
  145. };
  146. export async function openAM2UpsellModalSamePrice(options: UpsellModalSamePriceProps) {
  147. const {default: Modal, modalCss} = await import(
  148. 'getsentry/components/upgradeNowModal/modalSamePrice'
  149. );
  150. openModal(deps => <Modal {...deps} {...options} />, {modalCss});
  151. }
  152. type ProfilingUpsellModalProps = {
  153. organization: Organization;
  154. subscription: Subscription;
  155. isActionDisabled?: boolean;
  156. onComplete?: () => void;
  157. };
  158. export async function openAM2ProfilingUpsellModal(options: ProfilingUpsellModalProps) {
  159. const {default: Modal, modalCss} = await import(
  160. 'getsentry/components/profiling/profilingUpgradeModal'
  161. );
  162. openModal(deps => <Modal {...deps} {...options} />, {modalCss});
  163. }
  164. type PromotionModalOptions = {
  165. organization: Organization;
  166. price: number;
  167. promotion: Promotion;
  168. promptFeature: string;
  169. PromotionModalBody?: ComponentType<PromotionModalBodyProps>;
  170. acceptButtonText?: string;
  171. api?: Client;
  172. declineButtonText?: string;
  173. onAccept?: () => void;
  174. };
  175. export async function openPromotionModal(options: PromotionModalOptions) {
  176. const {default: Modal, modalCss} = await import('getsentry/components/promotionModal');
  177. openModal(deps => <Modal {...deps} {...options} />, {closeEvents: 'none', modalCss});
  178. }
  179. export function openPromotionReminderModal(
  180. promotionClaimed: PromotionClaimed,
  181. onCancel?: () => void,
  182. onConfirm?: () => void
  183. ) {
  184. const {dateCompleted} = promotionClaimed;
  185. const promo = promotionClaimed.promotion;
  186. const {amount, billingInterval, billingPeriods, maxCentsPerPeriod, reminderText} =
  187. promo.discountInfo;
  188. const date = new Date(dateCompleted);
  189. const percentOff = amount / 100;
  190. const interval = billingInterval === 'monthly' ? t('months') : t('years');
  191. const intervalSingular = interval.slice(0, -1);
  192. /**
  193. * Removed translation because of complicated pluralization and lots of changing
  194. * parameters from the different promotions we can use this for
  195. */
  196. openConfirmModal({
  197. message: (
  198. <div>
  199. <p>{reminderText}</p>
  200. <Subheader>{t('Current Promotion:')} </Subheader>
  201. <p>
  202. {percentOff}% off (up to ${maxCentsPerPeriod / 100} per {intervalSingular}) for{' '}
  203. {billingPeriods} {interval} starting on {date.toLocaleDateString('en-US')}
  204. </p>
  205. </div>
  206. ),
  207. header: <HeaderText>Promotion Conflict</HeaderText>,
  208. priority: 'danger',
  209. confirmText: 'Downgrade Anyway',
  210. onCancel: () => onCancel?.(),
  211. onConfirm: () => onConfirm?.(),
  212. });
  213. }
  214. export async function openCodecovModal(options: {organization: Organization}) {
  215. const {default: Modal, modalCss} = await import(
  216. 'getsentry/components/codecovPromotionModal'
  217. );
  218. openModal(deps => <Modal {...deps} {...options} />, {modalCss, closeEvents: 'none'});
  219. }
  220. const HeaderText = styled('div')`
  221. font-size: ${p => p.theme.fontSizeExtraLarge};
  222. font-weight: bold;
  223. `;
  224. const Subheader = styled('div')`
  225. font-weight: bold;
  226. font-size: ${p => p.theme.fontSizeMedium};
  227. `;
  228. export async function openDataConsentModal() {
  229. const {default: Modal} = await import('getsentry/components/dataConsentModal');
  230. openModal(deps => <Modal {...deps} />);
  231. }