root.tsx 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. import { useCallback } from 'react'
  2. import moment from 'moment'
  3. import { useTranslation, Trans } from 'react-i18next'
  4. import {
  5. SubscriptionChangePreview,
  6. AddOnPurchase,
  7. PremiumSubscriptionChange,
  8. } from '../../../../../../types/subscription/subscription-change-preview'
  9. import getMeta from '@/utils/meta'
  10. import { formatCurrency } from '@/shared/utils/currency'
  11. import useAsync from '@/shared/hooks/use-async'
  12. import { useLocation } from '@/shared/hooks/use-location'
  13. import { debugConsole } from '@/utils/debugging'
  14. import { postJSON } from '@/infrastructure/fetch-json'
  15. import Notification from '@/shared/components/notification'
  16. import OLCard from '@/features/ui/components/ol/ol-card'
  17. import OLRow from '@/features/ui/components/ol/ol-row'
  18. import OLCol from '@/features/ui/components/ol/ol-col'
  19. import OLButton from '@/features/ui/components/ol/ol-button'
  20. import { subscriptionUpdateUrl } from '@/features/subscription/data/subscription-url'
  21. import * as eventTracking from '@/infrastructure/event-tracking'
  22. import sparkleText from '@/shared/svgs/ai-sparkle-text.svg'
  23. function PreviewSubscriptionChange() {
  24. const preview = getMeta(
  25. 'ol-subscriptionChangePreview'
  26. ) as SubscriptionChangePreview
  27. const purchaseReferrer = getMeta('ol-purchaseReferrer')
  28. const { t } = useTranslation()
  29. const payNowTask = useAsync()
  30. const location = useLocation()
  31. const handlePayNowClick = useCallback(() => {
  32. let addOnSegmentation: Record<any, unknown> | null = null
  33. if (preview.change.type === 'add-on-purchase') {
  34. addOnSegmentation = {
  35. addOn: preview.change.addOn.code,
  36. upgradeType: 'add-on',
  37. }
  38. if (purchaseReferrer) {
  39. addOnSegmentation.referrer = purchaseReferrer
  40. }
  41. eventTracking.sendMB('subscription-change-form-submit', addOnSegmentation)
  42. }
  43. eventTracking.sendMB('assistant-add-on-purchase')
  44. payNowTask
  45. .runAsync(payNow(preview))
  46. .then(() => {
  47. if (addOnSegmentation) {
  48. eventTracking.sendMB(
  49. 'subscription-change-form-success',
  50. addOnSegmentation
  51. )
  52. }
  53. location.replace('/user/subscription/thank-you')
  54. })
  55. .catch(debugConsole.error)
  56. }, [purchaseReferrer, location, payNowTask, preview])
  57. const aiAddOnChange =
  58. preview.change.type === 'add-on-purchase' &&
  59. preview.change.addOn.code === 'assistant'
  60. // the driver of the change, which we can display as the immediate charge
  61. const changeName =
  62. preview.change.type === 'add-on-purchase'
  63. ? (preview.change as AddOnPurchase).addOn.name
  64. : (preview.change as PremiumSubscriptionChange).plan.name
  65. return (
  66. <div className="container">
  67. <OLRow>
  68. <OLCol md={{ offset: 2, span: 8 }}>
  69. <OLCard className="p-3">
  70. {preview.change.type === 'add-on-purchase' ? (
  71. <h1>
  72. {t('add_add_on_to_your_plan', {
  73. addOnName: preview.change.addOn.name,
  74. })}
  75. </h1>
  76. ) : preview.change.type === 'premium-subscription' ? (
  77. <h1>
  78. {t('subscribe_to_plan', { planName: preview.change.plan.name })}
  79. </h1>
  80. ) : null}
  81. {payNowTask.isError && (
  82. <Notification
  83. type="error"
  84. aria-live="polite"
  85. content={
  86. <>
  87. {t('generic_something_went_wrong')}. {t('try_again')}.{' '}
  88. {t('generic_if_problem_continues_contact_us')}.
  89. </>
  90. }
  91. />
  92. )}
  93. {aiAddOnChange && (
  94. <div>
  95. <Trans
  96. i18nKey="add_error_assist_to_your_projects"
  97. components={{
  98. sparkle: (
  99. <img
  100. alt="sparkle"
  101. className="ai-error-assistant-sparkle"
  102. src={sparkleText}
  103. aria-hidden="true"
  104. key="sparkle"
  105. />
  106. ),
  107. }}
  108. />
  109. </div>
  110. )}
  111. <OLCard className="payment-summary-card mt-5">
  112. <h3>{t('due_today')}:</h3>
  113. <OLRow>
  114. <OLCol xs={9}>{changeName}</OLCol>
  115. <OLCol xs={3} className="text-end">
  116. <strong>
  117. {formatCurrency(
  118. preview.immediateCharge.subtotal,
  119. preview.currency
  120. )}
  121. </strong>
  122. </OLCol>
  123. </OLRow>
  124. {preview.immediateCharge.tax > 0 && (
  125. <OLRow className="mt-1">
  126. <OLCol xs={9}>
  127. {t('vat')} {preview.nextInvoice.tax.rate * 100}%
  128. </OLCol>
  129. <OLCol xs={3} className="text-end">
  130. {formatCurrency(
  131. preview.immediateCharge.tax,
  132. preview.currency
  133. )}
  134. </OLCol>
  135. </OLRow>
  136. )}
  137. <OLRow className="mt-1">
  138. <OLCol xs={9}>{t('total_today')}</OLCol>
  139. <OLCol xs={3} className="text-end">
  140. <strong>
  141. {formatCurrency(
  142. preview.immediateCharge.total,
  143. preview.currency
  144. )}
  145. </strong>
  146. </OLCol>
  147. </OLRow>
  148. </OLCard>
  149. <div className="mt-5">
  150. <Trans
  151. i18nKey="this_total_reflects_the_amount_due_until"
  152. values={{ date: moment(preview.nextInvoice.date).format('LL') }}
  153. components={{ strong: <strong /> }}
  154. shouldUnescape
  155. tOptions={{ interpolation: { escapeValue: true } }}
  156. />{' '}
  157. <Trans
  158. i18nKey="we_will_use_your_existing_payment_method"
  159. values={{ paymentMethod: preview.paymentMethod }}
  160. components={{ strong: <strong /> }}
  161. shouldUnescape
  162. tOptions={{ interpolation: { escapeValue: true } }}
  163. />
  164. </div>
  165. <div className="mt-5">
  166. <OLButton
  167. variant="primary"
  168. size="lg"
  169. onClick={handlePayNowClick}
  170. disabled={payNowTask.isLoading || payNowTask.isSuccess}
  171. >
  172. {t('pay_now')}
  173. </OLButton>
  174. </div>
  175. <OLCard className="payment-summary-card mt-5">
  176. <h3>{t('future_payments')}:</h3>
  177. <OLRow className="mt-1">
  178. <OLCol xs={9}>{preview.nextInvoice.plan.name}</OLCol>
  179. <OLCol xs={3} className="text-end">
  180. {formatCurrency(
  181. preview.nextInvoice.plan.amount,
  182. preview.currency
  183. )}
  184. </OLCol>
  185. </OLRow>
  186. {preview.nextInvoice.addOns.map(addOn => (
  187. <OLRow className="mt-1" key={addOn.code}>
  188. <OLCol xs={9}>
  189. {addOn.name}
  190. {addOn.quantity > 1 ? ` ×${addOn.quantity}` : ''}
  191. </OLCol>
  192. <OLCol xs={3} className="text-end">
  193. {formatCurrency(addOn.amount, preview.currency)}
  194. </OLCol>
  195. </OLRow>
  196. ))}
  197. {preview.nextInvoice.tax.rate > 0 && (
  198. <OLRow className="mt-1">
  199. <OLCol xs={9}>
  200. {t('vat')} {preview.nextInvoice.tax.rate * 100}%
  201. </OLCol>
  202. <OLCol xs={3} className="text-end">
  203. {formatCurrency(
  204. preview.nextInvoice.tax.amount,
  205. preview.currency
  206. )}
  207. </OLCol>
  208. </OLRow>
  209. )}
  210. <OLRow className="mt-1">
  211. <OLCol xs={9}>
  212. {preview.nextPlan.annual
  213. ? t('total_per_year')
  214. : t('total_per_month')}
  215. </OLCol>
  216. <OLCol xs={3} className="text-end">
  217. {formatCurrency(preview.nextInvoice.total, preview.currency)}
  218. </OLCol>
  219. </OLRow>
  220. </OLCard>
  221. <div className="mt-5">
  222. <Trans
  223. i18nKey="the_next_payment_will_be_collected_on"
  224. values={{ date: moment(preview.nextInvoice.date).format('LL') }}
  225. components={{ strong: <strong /> }}
  226. shouldUnescape
  227. tOptions={{ interpolation: { escapeValue: true } }}
  228. />
  229. </div>
  230. </OLCard>
  231. </OLCol>
  232. </OLRow>
  233. </div>
  234. )
  235. }
  236. async function payNow(preview: SubscriptionChangePreview) {
  237. if (preview.change.type === 'add-on-purchase') {
  238. await postJSON(`/user/subscription/addon/${preview.change.addOn.code}/add`)
  239. } else if (preview.change.type === 'premium-subscription') {
  240. await postJSON(subscriptionUpdateUrl, {
  241. body: { plan_code: preview.change.plan.code },
  242. })
  243. } else {
  244. throw new Error(
  245. `Unknown subscription change preview type: ${preview.change}`
  246. )
  247. }
  248. }
  249. export default PreviewSubscriptionChange