compile-timeout-paywall-modal.tsx 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. import '../../../../stylesheets/components/compile-time-paywall-modal.scss'
  2. import {
  3. OLModal,
  4. OLModalBody,
  5. OLModalHeader,
  6. OLModalTitle,
  7. } from '@/shared/components/ol/ol-modal'
  8. import OLButton from '@/shared/components/ol/ol-button'
  9. import MaterialIcon from '@/shared/components/material-icon'
  10. import BillingPeriodToggle, {
  11. type BillingPeriod,
  12. } from '@/shared/components/billing-period-toggle'
  13. import getMeta from '@/utils/meta'
  14. import { sendMB } from '@/infrastructure/event-tracking'
  15. import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
  16. import { useTranslation } from 'react-i18next'
  17. const PLAN_CODE = {
  18. monthly: 'collaborator',
  19. annual: 'collaborator-annual',
  20. } as const
  21. export type CompileTimeoutPaywallModalProps = {
  22. show: boolean
  23. onHide: () => void
  24. }
  25. export function CompileTimeoutPaywallModal({
  26. show,
  27. onHide,
  28. }: CompileTimeoutPaywallModalProps) {
  29. const { t } = useTranslation()
  30. const standardPlanPricing = getMeta('ol-standardPlanPricing')
  31. const currency = getMeta('ol-recommendedCurrency')
  32. const supportsAnnualPricing = Boolean(standardPlanPricing?.annual)
  33. const [annual, setAnnual] = useState(false)
  34. useEffect(() => {
  35. if (!show) {
  36. setAnnual(false)
  37. }
  38. }, [show])
  39. const billingPeriod: BillingPeriod = annual ? 'annual' : 'monthly'
  40. const planCode = PLAN_CODE[billingPeriod]
  41. const hasShownRef = useRef(false)
  42. useEffect(() => {
  43. if (show && !hasShownRef.current) {
  44. const countryCode = getMeta('ol-countryCode')
  45. sendMB('paywall-plans-page-view', {
  46. 'paywall-type': 'compile-timeout',
  47. currency,
  48. countryCode,
  49. version: 'compile-timeout',
  50. })
  51. hasShownRef.current = true
  52. } else if (!show) {
  53. hasShownRef.current = false
  54. }
  55. }, [show, currency])
  56. const handleToggleBilling = useCallback(() => {
  57. const nextAnnual = !annual
  58. setAnnual(nextAnnual)
  59. sendMB('paywall-plans-page-toggle', {
  60. 'billing-period': nextAnnual ? 'annual' : 'monthly',
  61. checked: nextAnnual ? 'checked' : 'unchecked',
  62. version: 'compile-timeout',
  63. })
  64. }, [annual])
  65. const openCheckout = useCallback(() => {
  66. const params = new URLSearchParams({
  67. itm_campaign: 'compile-timeout',
  68. })
  69. window.open(
  70. `/user/subscription/new?planCode=${planCode}&${params.toString()}`,
  71. '_blank',
  72. 'noopener,noreferrer'
  73. )
  74. }, [planCode])
  75. const handleUpgrade = useCallback(() => {
  76. sendMB('paywall-plans-page-click', {
  77. 'paywall-type': 'compile-timeout',
  78. plan: 'collaborator',
  79. 'billing-period': billingPeriod,
  80. currency,
  81. version: 'compile-timeout',
  82. button: 'buy',
  83. })
  84. openCheckout()
  85. }, [billingPeriod, currency, openCheckout])
  86. const viewAllPlansHref = useMemo(() => {
  87. const params = new URLSearchParams({
  88. 'paywall-type': 'compile-timeout',
  89. itm_campaign: 'compile-timeout',
  90. })
  91. return `/user/subscription/choose-your-plan?${params.toString()}`
  92. }, [])
  93. const handleViewAllPlans = useCallback(() => {
  94. sendMB('paywall-plans-page-click', {
  95. 'paywall-type': 'compile-timeout',
  96. button: 'plans',
  97. 'billing-period': billingPeriod,
  98. currency,
  99. version: 'compile-timeout',
  100. })
  101. }, [billingPeriod, currency])
  102. const price = annual
  103. ? standardPlanPricing?.annual
  104. : standardPlanPricing?.monthly
  105. const priceSubtext = annual ? t('per_year') : t('per_month')
  106. const strikethroughPrice = annual
  107. ? standardPlanPricing?.monthlyTimesTwelve
  108. : undefined
  109. return (
  110. <OLModal
  111. className="compile-time-paywall-modal"
  112. size="sm"
  113. show={show}
  114. onHide={onHide}
  115. centered
  116. >
  117. <OLModalHeader className="border-0 pb-0" closeButton>
  118. <div className="w-100 text-center">
  119. <OLModalTitle>{t('get_more_compile_time')}</OLModalTitle>
  120. </div>
  121. </OLModalHeader>
  122. <OLModalBody>
  123. <div className="compile-time-paywall-card">
  124. <div className="compile-time-paywall-header">
  125. <div className="compile-time-paywall-plan-meta">
  126. <p className="compile-time-paywall-plan-label">
  127. {t('standard')} {t('plan')}
  128. </p>
  129. <div className="compile-time-paywall-price-container">
  130. {strikethroughPrice ? (
  131. <span className="compile-time-paywall-price-strikethrough">
  132. {strikethroughPrice}
  133. </span>
  134. ) : (
  135. <span
  136. className="compile-time-paywall-price-strikethrough"
  137. style={{ visibility: 'hidden' }}
  138. aria-hidden="true"
  139. >
  140. &nbsp;
  141. </span>
  142. )}
  143. <span className="compile-time-paywall-price">{price}</span>
  144. {priceSubtext && (
  145. <span className="compile-time-paywall-price-subtext">
  146. {priceSubtext}
  147. </span>
  148. )}
  149. </div>
  150. </div>
  151. {supportsAnnualPricing && (
  152. <div className="compile-time-paywall-period-toggle">
  153. <BillingPeriodToggle
  154. id="compile-timeout-period"
  155. value={annual ? 'annual' : 'monthly'}
  156. onChange={(period: BillingPeriod) => {
  157. if ((period === 'annual') !== annual) {
  158. handleToggleBilling()
  159. }
  160. }}
  161. variant="premium"
  162. />
  163. </div>
  164. )}
  165. </div>
  166. <div className="compile-time-paywall-body">
  167. <div className="compile-time-paywall-illustration">
  168. <MaterialIcon type="rocket_launch" />
  169. </div>
  170. <div className="compile-time-paywall-content">
  171. <p className="compile-time-paywall-intro">
  172. <strong>{t('compile_timeout_modal_intro')}</strong>
  173. </p>
  174. <div className="compile-time-paywall-feature-list">
  175. <ListItem>{t('collabs_per_proj', { count: 10 })}</ListItem>
  176. <ListItem>{t('track_changes')}</ListItem>
  177. <ListItem>{t('github_integration')}</ListItem>
  178. <ListItem>{t('and_much_more')}</ListItem>
  179. </div>
  180. </div>
  181. </div>
  182. <div className="compile-time-paywall-actions">
  183. <OLButton
  184. variant="premium"
  185. type="button"
  186. size="lg"
  187. onClick={handleUpgrade}
  188. >
  189. {t('buy_now_no_exclamation_mark')}
  190. </OLButton>
  191. <OLButton
  192. className="compile-time-paywall-view-plans"
  193. variant="ghost"
  194. href={viewAllPlansHref}
  195. target="_blank"
  196. rel="noreferrer"
  197. onClick={handleViewAllPlans}
  198. >
  199. {t('view_all_plans')}
  200. </OLButton>
  201. </div>
  202. </div>
  203. </OLModalBody>
  204. </OLModal>
  205. )
  206. }
  207. function ListItem({ children }: { children: React.ReactNode }) {
  208. return (
  209. <div className="compile-time-paywall-list-item">
  210. <MaterialIcon type="check_circle" />
  211. <span>{children}</span>
  212. </div>
  213. )
  214. }
  215. export default CompileTimeoutPaywallModal