subscription-dashboard-context.tsx 11 KB

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