subscription-dashboard-context.tsx 11 KB

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