subscription-dashboard-context.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. import {
  2. createContext,
  3. ReactNode,
  4. useCallback,
  5. useContext,
  6. useEffect,
  7. useMemo,
  8. useState,
  9. } from 'react'
  10. import { useTranslation } from 'react-i18next'
  11. import {
  12. CustomSubscription,
  13. ManagedGroupSubscription,
  14. MemberGroupSubscription,
  15. PaidSubscription,
  16. } from '../../../../../types/subscription/dashboard/subscription'
  17. import {
  18. Plan,
  19. PriceForDisplayData,
  20. } from '../../../../../types/subscription/plan'
  21. import { Institution } from '../../../../../types/institution'
  22. import getMeta from '../../../utils/meta'
  23. import {
  24. loadDisplayPriceWithTaxPromise,
  25. loadGroupDisplayPriceWithTaxForRecurlyPromise,
  26. loadGroupDisplayPriceWithTaxForStripePromise,
  27. } from '../util/recurly-pricing'
  28. import { isRecurlyLoaded } from '../util/is-recurly-loaded'
  29. import { SubscriptionDashModalIds } from '../../../../../types/subscription/dashboard/modal-ids'
  30. import { debugConsole } from '@/utils/debugging'
  31. import { formatCurrency } from '@/shared/utils/currency'
  32. import { ManagedInstitution } from '../../../../../types/subscription/dashboard/managed-institution'
  33. import { Publisher } from '../../../../../types/subscription/dashboard/publisher'
  34. import { formatTime } from '@/features/utils/format-date'
  35. type SubscriptionDashboardContextValue = {
  36. groupPlanToChangeToCode: string
  37. groupPlanToChangeToSize: string
  38. groupPlanToChangeToUsage: string
  39. groupPlanToChangeToPrice?: PriceForDisplayData
  40. groupPlanToChangeToPriceError?: boolean
  41. handleCloseModal: () => void
  42. handleOpenModal: (
  43. modalIdToOpen: SubscriptionDashModalIds,
  44. planCode?: string
  45. ) => void
  46. hasDisplayedSubscription: boolean
  47. hasValidActiveSubscription: boolean
  48. institutionMemberships?: Institution[]
  49. managedGroupSubscriptions: ManagedGroupSubscription[]
  50. memberGroupSubscriptions: MemberGroupSubscription[]
  51. managedInstitutions: ManagedInstitution[]
  52. managedPublishers: Publisher[]
  53. updateManagedInstitution: (institution: ManagedInstitution) => void
  54. modalIdShown?: SubscriptionDashModalIds
  55. personalSubscription?: PaidSubscription | CustomSubscription
  56. hasSubscription: boolean
  57. plans: Plan[]
  58. planCodeToChangeTo?: string
  59. queryingGroupPlanToChangeToPrice: boolean
  60. queryingIndividualPlansData: boolean
  61. recurlyLoadError: boolean
  62. setGroupPlanToChangeToCode: React.Dispatch<React.SetStateAction<string>>
  63. setGroupPlanToChangeToSize: React.Dispatch<React.SetStateAction<string>>
  64. setGroupPlanToChangeToUsage: React.Dispatch<React.SetStateAction<string>>
  65. setModalIdShown: React.Dispatch<
  66. React.SetStateAction<SubscriptionDashModalIds | undefined>
  67. >
  68. setPlanCodeToChangeTo: React.Dispatch<
  69. React.SetStateAction<string | undefined>
  70. >
  71. setRecurlyLoadError: React.Dispatch<React.SetStateAction<boolean>>
  72. showCancellation: boolean
  73. setShowCancellation: React.Dispatch<React.SetStateAction<boolean>>
  74. leavingGroupId?: string
  75. setLeavingGroupId: React.Dispatch<React.SetStateAction<string | undefined>>
  76. userCanExtendTrial: boolean
  77. getFormattedRenewalDate: () => string
  78. }
  79. export const SubscriptionDashboardContext = createContext<
  80. SubscriptionDashboardContextValue | undefined
  81. >(undefined)
  82. export function SubscriptionDashboardProvider({
  83. children,
  84. }: {
  85. children: ReactNode
  86. }) {
  87. const { i18n } = useTranslation()
  88. const [modalIdShown, setModalIdShown] = useState<
  89. SubscriptionDashModalIds | undefined
  90. >()
  91. const [recurlyLoadError, setRecurlyLoadError] = useState(false)
  92. const [showCancellation, setShowCancellation] = useState(false)
  93. const [plans, setPlans] = useState<Plan[]>([])
  94. const [queryingIndividualPlansData, setQueryingIndividualPlansData] =
  95. useState(true)
  96. const [planCodeToChangeTo, setPlanCodeToChangeTo] = useState<
  97. string | undefined
  98. >()
  99. const [groupPlanToChangeToSize, setGroupPlanToChangeToSize] = useState('10')
  100. const [groupPlanToChangeToCode, setGroupPlanToChangeToCode] =
  101. useState('collaborator')
  102. const [groupPlanToChangeToUsage, setGroupPlanToChangeToUsage] =
  103. useState('enterprise')
  104. const [
  105. queryingGroupPlanToChangeToPrice,
  106. setQueryingGroupPlanToChangeToPrice,
  107. ] = useState(false)
  108. const [groupPlanToChangeToPrice, setGroupPlanToChangeToPrice] =
  109. useState<PriceForDisplayData>()
  110. const [groupPlanToChangeToPriceError, setGroupPlanToChangeToPriceError] =
  111. useState(false)
  112. const [leavingGroupId, setLeavingGroupId] = useState<string | undefined>()
  113. const plansWithoutDisplayPrice = getMeta('ol-plans')
  114. const institutionMemberships = getMeta('ol-currentInstitutionsWithLicence')
  115. const personalSubscription = getMeta('ol-subscription')
  116. const userCanExtendTrial = getMeta('ol-userCanExtendTrial')
  117. const managedGroupSubscriptions = getMeta('ol-managedGroupSubscriptions')
  118. const memberGroupSubscriptions = getMeta('ol-memberGroupSubscriptions')
  119. const [managedInstitutions, setManagedInstitutions] = useState(
  120. getMeta('ol-managedInstitutions')
  121. )
  122. const managedPublishers = getMeta('ol-managedPublishers')
  123. const hasSubscription = getMeta('ol-hasSubscription')
  124. const recurlyApiKey = getMeta('ol-recurlyApiKey')
  125. const hasDisplayedSubscription = Boolean(
  126. institutionMemberships?.length > 0 ||
  127. personalSubscription ||
  128. memberGroupSubscriptions?.length > 0 ||
  129. managedGroupSubscriptions?.length > 0 ||
  130. managedInstitutions?.length > 0 ||
  131. managedPublishers?.length > 0
  132. )
  133. const hasValidActiveSubscription = Boolean(
  134. ['active', 'canceled'].includes(personalSubscription?.payment?.state) ||
  135. institutionMemberships?.length > 0 ||
  136. memberGroupSubscriptions?.length > 0
  137. )
  138. const getFormattedRenewalDate = useCallback(() => {
  139. if (
  140. !personalSubscription.payment.pausedAt ||
  141. !personalSubscription.payment.remainingPauseCycles
  142. ) {
  143. return personalSubscription.payment.nextPaymentDueAt
  144. }
  145. const pausedDate = new Date(personalSubscription.payment.pausedAt)
  146. pausedDate.setMonth(
  147. pausedDate.getMonth() + personalSubscription.payment.remainingPauseCycles
  148. )
  149. return formatTime(pausedDate, 'MMMM Do, YYYY')
  150. }, [personalSubscription])
  151. useEffect(() => {
  152. if (!isRecurlyLoaded()) {
  153. setRecurlyLoadError(true)
  154. } else if (recurlyApiKey) {
  155. recurly.configure(recurlyApiKey)
  156. }
  157. }, [recurlyApiKey, setRecurlyLoadError])
  158. useEffect(() => {
  159. if (
  160. isRecurlyLoaded() &&
  161. plansWithoutDisplayPrice &&
  162. personalSubscription?.payment
  163. ) {
  164. const { currency, taxRate } = personalSubscription.payment
  165. const fetchPlansDisplayPrices = async () => {
  166. for (const plan of plansWithoutDisplayPrice) {
  167. try {
  168. const priceData = await loadDisplayPriceWithTaxPromise(
  169. plan.planCode,
  170. currency,
  171. taxRate,
  172. i18n.language
  173. )
  174. if (priceData?.totalAsNumber !== undefined) {
  175. plan.displayPrice = formatCurrency(
  176. priceData.totalAsNumber,
  177. currency,
  178. i18n.language
  179. )
  180. }
  181. } catch (error) {
  182. debugConsole.error(error)
  183. }
  184. }
  185. setPlans(plansWithoutDisplayPrice)
  186. setQueryingIndividualPlansData(false)
  187. }
  188. fetchPlansDisplayPrices().catch(debugConsole.error)
  189. }
  190. }, [personalSubscription, plansWithoutDisplayPrice, i18n.language])
  191. useEffect(() => {
  192. if (
  193. !groupPlanToChangeToCode ||
  194. !groupPlanToChangeToSize ||
  195. !groupPlanToChangeToUsage ||
  196. !personalSubscription?.payment
  197. ) {
  198. return
  199. }
  200. let loadGroupDisplayPrice
  201. if (personalSubscription.service?.includes('stripe')) {
  202. loadGroupDisplayPrice = loadGroupDisplayPriceWithTaxForStripePromise
  203. } else if (isRecurlyLoaded()) {
  204. loadGroupDisplayPrice = loadGroupDisplayPriceWithTaxForRecurlyPromise
  205. } else {
  206. return
  207. }
  208. setQueryingGroupPlanToChangeToPrice(true)
  209. const { currency, taxRate } = personalSubscription.payment
  210. const fetchGroupDisplayPrice = async () => {
  211. setGroupPlanToChangeToPriceError(false)
  212. let priceData
  213. try {
  214. priceData = await loadGroupDisplayPrice(
  215. groupPlanToChangeToCode,
  216. currency,
  217. taxRate,
  218. groupPlanToChangeToSize,
  219. groupPlanToChangeToUsage,
  220. i18n.language
  221. )
  222. } catch (e) {
  223. debugConsole.error(e)
  224. setGroupPlanToChangeToPriceError(true)
  225. }
  226. setQueryingGroupPlanToChangeToPrice(false)
  227. setGroupPlanToChangeToPrice(priceData)
  228. }
  229. fetchGroupDisplayPrice()
  230. }, [
  231. groupPlanToChangeToUsage,
  232. groupPlanToChangeToSize,
  233. personalSubscription,
  234. groupPlanToChangeToCode,
  235. i18n.language,
  236. ])
  237. const updateManagedInstitution = useCallback(
  238. (institution: ManagedInstitution) => {
  239. setManagedInstitutions(institutions => {
  240. return [
  241. ...(institutions || []).map(i =>
  242. i.v1Id === institution.v1Id ? institution : i
  243. ),
  244. ]
  245. })
  246. },
  247. []
  248. )
  249. const handleCloseModal = useCallback(() => {
  250. setModalIdShown(undefined)
  251. setPlanCodeToChangeTo(undefined)
  252. }, [setModalIdShown, setPlanCodeToChangeTo])
  253. const handleOpenModal = useCallback(
  254. (id: SubscriptionDashModalIds, planCode?: string) => {
  255. setModalIdShown(id)
  256. setPlanCodeToChangeTo(planCode)
  257. },
  258. [setModalIdShown, setPlanCodeToChangeTo]
  259. )
  260. const value = useMemo<SubscriptionDashboardContextValue>(
  261. () => ({
  262. groupPlanToChangeToCode,
  263. groupPlanToChangeToPrice,
  264. groupPlanToChangeToPriceError,
  265. groupPlanToChangeToSize,
  266. groupPlanToChangeToUsage,
  267. handleCloseModal,
  268. handleOpenModal,
  269. hasDisplayedSubscription,
  270. hasValidActiveSubscription,
  271. institutionMemberships,
  272. managedGroupSubscriptions,
  273. memberGroupSubscriptions,
  274. managedInstitutions,
  275. managedPublishers,
  276. updateManagedInstitution,
  277. modalIdShown,
  278. personalSubscription,
  279. hasSubscription,
  280. plans,
  281. planCodeToChangeTo,
  282. queryingGroupPlanToChangeToPrice,
  283. queryingIndividualPlansData,
  284. recurlyLoadError,
  285. setGroupPlanToChangeToCode,
  286. setGroupPlanToChangeToSize,
  287. setGroupPlanToChangeToUsage,
  288. setModalIdShown,
  289. setPlanCodeToChangeTo,
  290. setRecurlyLoadError,
  291. showCancellation,
  292. setShowCancellation,
  293. leavingGroupId,
  294. setLeavingGroupId,
  295. userCanExtendTrial,
  296. getFormattedRenewalDate,
  297. }),
  298. [
  299. groupPlanToChangeToCode,
  300. groupPlanToChangeToPrice,
  301. groupPlanToChangeToPriceError,
  302. groupPlanToChangeToSize,
  303. groupPlanToChangeToUsage,
  304. handleCloseModal,
  305. handleOpenModal,
  306. hasDisplayedSubscription,
  307. hasValidActiveSubscription,
  308. institutionMemberships,
  309. managedGroupSubscriptions,
  310. memberGroupSubscriptions,
  311. managedInstitutions,
  312. managedPublishers,
  313. updateManagedInstitution,
  314. modalIdShown,
  315. personalSubscription,
  316. hasSubscription,
  317. plans,
  318. planCodeToChangeTo,
  319. queryingGroupPlanToChangeToPrice,
  320. queryingIndividualPlansData,
  321. recurlyLoadError,
  322. setGroupPlanToChangeToCode,
  323. setGroupPlanToChangeToSize,
  324. setGroupPlanToChangeToUsage,
  325. setModalIdShown,
  326. setPlanCodeToChangeTo,
  327. setRecurlyLoadError,
  328. showCancellation,
  329. setShowCancellation,
  330. leavingGroupId,
  331. setLeavingGroupId,
  332. userCanExtendTrial,
  333. getFormattedRenewalDate,
  334. ]
  335. )
  336. return (
  337. <SubscriptionDashboardContext.Provider value={value}>
  338. {children}
  339. </SubscriptionDashboardContext.Provider>
  340. )
  341. }
  342. export function useSubscriptionDashboardContext() {
  343. const context = useContext(SubscriptionDashboardContext)
  344. if (!context) {
  345. throw new Error(
  346. 'SubscriptionDashboardContext is only available inside SubscriptionDashboardProvider'
  347. )
  348. }
  349. return context
  350. }