subscription-dashboard-context.tsx 11 KB

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