subscription-dashboard-context.tsx 10 KB

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