subscription-dashboard-context.tsx 10 KB

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