root.tsx 8.3 KB

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