compile-timeout-paywall-modal.tsx 7.1 KB

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