mirror of
https://github.com/overleaf/overleaf.git
synced 2024-10-17 21:05:04 -04:00
b2ef7a935f
* Add a unit test on `SubscriptionFormatters.formatPrice` * Add JSDoc to `formatPrice` Also: Name the functions before exporting: This fixes my IDE (WebStorm) navigation * Make `'USD'` the default param instead of reassigning * Create `formatCurrency` function * Use `formatCurrency` in SubscriptionFormatters * Use an `isNoCentsCurrency` logic for `CLP` `JPY` `KRW` `VND` And remove custom `CLP` logic and locale * Add `locale` param to `formatPrice` * Generate `groups.json` and `localizedPlanPricing.json` ``` bin/exec web node ./scripts/recurly/recurly_prices.js --download -o prices.json bin/exec web node ./scripts/plan-prices/plans.js -f ../../prices.json -o dir ``` * Update scripts/plan-prices/plans.js to generate numbers instead of localized amounts * Generate `groups.json` and `localizedPlanPricing.json` ``` bin/exec web node ./scripts/recurly/recurly_prices.js --download -o prices.json bin/exec web node ./scripts/plan-prices/plans.js -f ../../prices.json -o dir ``` * Remove generation of `plans.json` As /services/web/frontend/js/main/plans.js was removed in https://github.com/overleaf/internal/pull/12593 * Sort currencies in alphabetical order in scripts/plan-prices/plans.js * Generate `groups.json` and `localizedPlanPricing.json` ``` bin/exec web node ./scripts/recurly/recurly_prices.js --download -o prices.json bin/exec web node ./scripts/plan-prices/plans.js -f ../../prices.json -o dir ``` * Use `formatCurrency` in price-summary.tsx * Use `formatCurrency` in Subscription Pug files * Fix unit tests SubscriptionHelperTests.js * Remove unused `currencySymbol` * Change to `formatCurrency` in other React components * Add `CurrencyCode` JSDoc types * Duplicate `formatCurrency` into services/web/app/src/util * Wrap tests in a top-level describe block * Use `narrowSymbol` * Fix tests with `narrowSymbol` expects * Revert deletion of old `formatPrice` in SubscriptionFormatters.js * Rename `formatCurrency` -> `formatCurrencyLocalized` * Revert deletion of `CurrencySymbol` * Add split-test in SubscriptionController.js * Add split-test in SubscriptionViewModelBuilder.js * Add split-test in plans * Add split-test in subscription-dashboard-context.tsx * Add split-test in 4 more components * Update tests * Show currency and payment methods in interstitial page * Fix `–` being printed. Use `–` instead * Fix test with NOK * Storybook: Fix missing `SplitTestProvider` * Storybook: Revert "Remove unused `currencySymbol`" This reverts commit e55387d4753f97bbf8e39e0fdc3ad17312122aaa. * Replace `getSplitTestVariant` by `useSplitTestContext` * Use parameterize currencyFormat in `generateInitialLocalizedGroupPrice` * Fixup import paths of `formatCurrencyLocalized` * Replace `% 1 === 0` by `Number.isInteger` * Add comment explaining that any combinations of languages/currencies could happen * Fixup after rebase: import `useSplitTestContext` * Revert "Remove SplitTestProvider from subscription root" This reverts commit be9f378fda715b86589ab0759737581c72321d87. * Revert "Remove split test provider from some tests" This reverts commit 985522932b550cfd38fa6a4f4c3d2ebaee6ff7df. GitOrigin-RevId: 59a83cbbe0f7cc7e45f189c654e23fcf9bfa37af
353 lines
11 KiB
TypeScript
353 lines
11 KiB
TypeScript
import {
|
|
createContext,
|
|
ReactNode,
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useMemo,
|
|
useState,
|
|
} from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import {
|
|
CustomSubscription,
|
|
ManagedGroupSubscription,
|
|
MemberGroupSubscription,
|
|
RecurlySubscription,
|
|
} from '../../../../../types/subscription/dashboard/subscription'
|
|
import {
|
|
Plan,
|
|
PriceForDisplayData,
|
|
} from '../../../../../types/subscription/plan'
|
|
import { Institution } from '../../../../../types/institution'
|
|
import { Institution as ManagedInstitution } from '../components/dashboard/managed-institutions'
|
|
import { Publisher as ManagedPublisher } from '../components/dashboard/managed-publishers'
|
|
import getMeta from '../../../utils/meta'
|
|
import {
|
|
formatCurrencyDefault,
|
|
loadDisplayPriceWithTaxPromise,
|
|
loadGroupDisplayPriceWithTaxPromise,
|
|
} from '../util/recurly-pricing'
|
|
import { isRecurlyLoaded } from '../util/is-recurly-loaded'
|
|
import { SubscriptionDashModalIds } from '../../../../../types/subscription/dashboard/modal-ids'
|
|
import { debugConsole } from '@/utils/debugging'
|
|
import { getSplitTestVariant } from '@/utils/splitTestUtils'
|
|
import { formatCurrencyLocalized } from '@/shared/utils/currency'
|
|
|
|
type SubscriptionDashboardContextValue = {
|
|
groupPlanToChangeToCode: string
|
|
groupPlanToChangeToSize: string
|
|
groupPlanToChangeToUsage: string
|
|
groupPlanToChangeToPrice?: PriceForDisplayData
|
|
groupPlanToChangeToPriceError?: boolean
|
|
handleCloseModal: () => void
|
|
handleOpenModal: (
|
|
modalIdToOpen: SubscriptionDashModalIds,
|
|
planCode?: string
|
|
) => void
|
|
hasDisplayedSubscription: boolean
|
|
hasValidActiveSubscription: boolean
|
|
institutionMemberships?: Institution[]
|
|
managedGroupSubscriptions: ManagedGroupSubscription[]
|
|
memberGroupSubscriptions: MemberGroupSubscription[]
|
|
managedInstitutions: ManagedInstitution[]
|
|
managedPublishers: ManagedPublisher[]
|
|
updateManagedInstitution: (institution: ManagedInstitution) => void
|
|
modalIdShown?: SubscriptionDashModalIds
|
|
personalSubscription?: RecurlySubscription | CustomSubscription
|
|
hasSubscription: boolean
|
|
plans: Plan[]
|
|
planCodeToChangeTo?: string
|
|
queryingGroupPlanToChangeToPrice: boolean
|
|
queryingIndividualPlansData: boolean
|
|
recurlyLoadError: boolean
|
|
setGroupPlanToChangeToCode: React.Dispatch<React.SetStateAction<string>>
|
|
setGroupPlanToChangeToSize: React.Dispatch<React.SetStateAction<string>>
|
|
setGroupPlanToChangeToUsage: React.Dispatch<React.SetStateAction<string>>
|
|
setModalIdShown: React.Dispatch<
|
|
React.SetStateAction<SubscriptionDashModalIds | undefined>
|
|
>
|
|
setPlanCodeToChangeTo: React.Dispatch<
|
|
React.SetStateAction<string | undefined>
|
|
>
|
|
setRecurlyLoadError: React.Dispatch<React.SetStateAction<boolean>>
|
|
showCancellation: boolean
|
|
setShowCancellation: React.Dispatch<React.SetStateAction<boolean>>
|
|
leavingGroupId?: string
|
|
setLeavingGroupId: React.Dispatch<React.SetStateAction<string | undefined>>
|
|
}
|
|
|
|
export const SubscriptionDashboardContext = createContext<
|
|
SubscriptionDashboardContextValue | undefined
|
|
>(undefined)
|
|
|
|
const getFormatCurrencies = () =>
|
|
getSplitTestVariant('local-ccy-format') === 'enabled'
|
|
? formatCurrencyLocalized
|
|
: formatCurrencyDefault
|
|
|
|
export function SubscriptionDashboardProvider({
|
|
children,
|
|
}: {
|
|
children: ReactNode
|
|
}) {
|
|
const { i18n } = useTranslation()
|
|
const [modalIdShown, setModalIdShown] = useState<
|
|
SubscriptionDashModalIds | undefined
|
|
>()
|
|
const [recurlyLoadError, setRecurlyLoadError] = useState(false)
|
|
const [showCancellation, setShowCancellation] = useState(false)
|
|
const [plans, setPlans] = useState([])
|
|
const [queryingIndividualPlansData, setQueryingIndividualPlansData] =
|
|
useState(true)
|
|
const [planCodeToChangeTo, setPlanCodeToChangeTo] = useState<
|
|
string | undefined
|
|
>()
|
|
const [groupPlanToChangeToSize, setGroupPlanToChangeToSize] = useState('10')
|
|
const [groupPlanToChangeToCode, setGroupPlanToChangeToCode] =
|
|
useState('collaborator')
|
|
const [groupPlanToChangeToUsage, setGroupPlanToChangeToUsage] =
|
|
useState('enterprise')
|
|
const [
|
|
queryingGroupPlanToChangeToPrice,
|
|
setQueryingGroupPlanToChangeToPrice,
|
|
] = useState(false)
|
|
const [groupPlanToChangeToPrice, setGroupPlanToChangeToPrice] =
|
|
useState<PriceForDisplayData>()
|
|
const [groupPlanToChangeToPriceError, setGroupPlanToChangeToPriceError] =
|
|
useState(false)
|
|
const [leavingGroupId, setLeavingGroupId] = useState<string | undefined>()
|
|
|
|
const plansWithoutDisplayPrice = getMeta('ol-plans')
|
|
const institutionMemberships: Institution[] = getMeta(
|
|
'ol-currentInstitutionsWithLicence'
|
|
)
|
|
const personalSubscription = getMeta('ol-subscription')
|
|
const managedGroupSubscriptions: ManagedGroupSubscription[] = getMeta(
|
|
'ol-managedGroupSubscriptions'
|
|
)
|
|
const memberGroupSubscriptions: MemberGroupSubscription[] = getMeta(
|
|
'ol-memberGroupSubscriptions'
|
|
)
|
|
const [managedInstitutions, setManagedInstitutions] = useState<
|
|
ManagedInstitution[]
|
|
>(getMeta('ol-managedInstitutions'))
|
|
const managedPublishers = getMeta('ol-managedPublishers')
|
|
const hasSubscription = getMeta('ol-hasSubscription')
|
|
const recurlyApiKey = getMeta('ol-recurlyApiKey')
|
|
|
|
const hasDisplayedSubscription = Boolean(
|
|
institutionMemberships?.length > 0 ||
|
|
personalSubscription ||
|
|
memberGroupSubscriptions?.length > 0 ||
|
|
managedGroupSubscriptions?.length > 0 ||
|
|
managedInstitutions?.length > 0 ||
|
|
managedPublishers?.length > 0
|
|
)
|
|
|
|
const hasValidActiveSubscription = Boolean(
|
|
['active', 'canceled'].includes(personalSubscription?.recurly?.state) ||
|
|
institutionMemberships?.length > 0 ||
|
|
memberGroupSubscriptions?.length > 0
|
|
)
|
|
|
|
useEffect(() => {
|
|
if (!isRecurlyLoaded()) {
|
|
setRecurlyLoadError(true)
|
|
} else if (recurlyApiKey) {
|
|
recurly.configure(recurlyApiKey)
|
|
}
|
|
}, [recurlyApiKey, setRecurlyLoadError])
|
|
|
|
useEffect(() => {
|
|
if (
|
|
isRecurlyLoaded() &&
|
|
plansWithoutDisplayPrice &&
|
|
personalSubscription?.recurly
|
|
) {
|
|
const formatCurrency = getFormatCurrencies()
|
|
const { currency, taxRate } = personalSubscription.recurly
|
|
const fetchPlansDisplayPrices = async () => {
|
|
for (const plan of plansWithoutDisplayPrice) {
|
|
try {
|
|
const priceData = await loadDisplayPriceWithTaxPromise(
|
|
plan.planCode,
|
|
currency,
|
|
taxRate,
|
|
i18n.language,
|
|
formatCurrency
|
|
)
|
|
if (priceData?.totalAsNumber !== undefined) {
|
|
plan.displayPrice = formatCurrency(
|
|
priceData.totalAsNumber,
|
|
currency,
|
|
i18n.language
|
|
)
|
|
}
|
|
} catch (error) {
|
|
debugConsole.error(error)
|
|
}
|
|
}
|
|
setPlans(plansWithoutDisplayPrice)
|
|
setQueryingIndividualPlansData(false)
|
|
}
|
|
fetchPlansDisplayPrices().catch(debugConsole.error)
|
|
}
|
|
}, [personalSubscription, plansWithoutDisplayPrice, i18n.language])
|
|
|
|
useEffect(() => {
|
|
if (
|
|
isRecurlyLoaded() &&
|
|
groupPlanToChangeToCode &&
|
|
groupPlanToChangeToSize &&
|
|
groupPlanToChangeToUsage &&
|
|
personalSubscription?.recurly
|
|
) {
|
|
setQueryingGroupPlanToChangeToPrice(true)
|
|
|
|
const { currency, taxRate } = personalSubscription.recurly
|
|
const fetchGroupDisplayPrice = async () => {
|
|
setGroupPlanToChangeToPriceError(false)
|
|
let priceData
|
|
try {
|
|
const formatCurrency = getFormatCurrencies()
|
|
priceData = await loadGroupDisplayPriceWithTaxPromise(
|
|
groupPlanToChangeToCode,
|
|
currency,
|
|
taxRate,
|
|
groupPlanToChangeToSize,
|
|
groupPlanToChangeToUsage,
|
|
i18n.language,
|
|
formatCurrency
|
|
)
|
|
} catch (e) {
|
|
debugConsole.error(e)
|
|
setGroupPlanToChangeToPriceError(true)
|
|
}
|
|
setQueryingGroupPlanToChangeToPrice(false)
|
|
setGroupPlanToChangeToPrice(priceData)
|
|
}
|
|
fetchGroupDisplayPrice()
|
|
}
|
|
}, [
|
|
groupPlanToChangeToUsage,
|
|
groupPlanToChangeToSize,
|
|
personalSubscription,
|
|
groupPlanToChangeToCode,
|
|
i18n.language,
|
|
])
|
|
|
|
const updateManagedInstitution = useCallback(
|
|
(institution: ManagedInstitution) => {
|
|
setManagedInstitutions(institutions => {
|
|
return [
|
|
...(institutions || []).map(i =>
|
|
i.v1Id === institution.v1Id ? institution : i
|
|
),
|
|
]
|
|
})
|
|
},
|
|
[]
|
|
)
|
|
|
|
const handleCloseModal = useCallback(() => {
|
|
setModalIdShown(undefined)
|
|
setPlanCodeToChangeTo(undefined)
|
|
}, [setModalIdShown, setPlanCodeToChangeTo])
|
|
|
|
const handleOpenModal = useCallback(
|
|
(id, planCode) => {
|
|
setModalIdShown(id)
|
|
setPlanCodeToChangeTo(planCode)
|
|
},
|
|
[setModalIdShown, setPlanCodeToChangeTo]
|
|
)
|
|
|
|
const value = useMemo<SubscriptionDashboardContextValue>(
|
|
() => ({
|
|
groupPlanToChangeToCode,
|
|
groupPlanToChangeToPrice,
|
|
groupPlanToChangeToPriceError,
|
|
groupPlanToChangeToSize,
|
|
groupPlanToChangeToUsage,
|
|
handleCloseModal,
|
|
handleOpenModal,
|
|
hasDisplayedSubscription,
|
|
hasValidActiveSubscription,
|
|
institutionMemberships,
|
|
managedGroupSubscriptions,
|
|
memberGroupSubscriptions,
|
|
managedInstitutions,
|
|
managedPublishers,
|
|
updateManagedInstitution,
|
|
modalIdShown,
|
|
personalSubscription,
|
|
hasSubscription,
|
|
plans,
|
|
planCodeToChangeTo,
|
|
queryingGroupPlanToChangeToPrice,
|
|
queryingIndividualPlansData,
|
|
recurlyLoadError,
|
|
setGroupPlanToChangeToCode,
|
|
setGroupPlanToChangeToSize,
|
|
setGroupPlanToChangeToUsage,
|
|
setModalIdShown,
|
|
setPlanCodeToChangeTo,
|
|
setRecurlyLoadError,
|
|
showCancellation,
|
|
setShowCancellation,
|
|
leavingGroupId,
|
|
setLeavingGroupId,
|
|
}),
|
|
[
|
|
groupPlanToChangeToCode,
|
|
groupPlanToChangeToPrice,
|
|
groupPlanToChangeToPriceError,
|
|
groupPlanToChangeToSize,
|
|
groupPlanToChangeToUsage,
|
|
handleCloseModal,
|
|
handleOpenModal,
|
|
hasDisplayedSubscription,
|
|
hasValidActiveSubscription,
|
|
institutionMemberships,
|
|
managedGroupSubscriptions,
|
|
memberGroupSubscriptions,
|
|
managedInstitutions,
|
|
managedPublishers,
|
|
updateManagedInstitution,
|
|
modalIdShown,
|
|
personalSubscription,
|
|
hasSubscription,
|
|
plans,
|
|
planCodeToChangeTo,
|
|
queryingGroupPlanToChangeToPrice,
|
|
queryingIndividualPlansData,
|
|
recurlyLoadError,
|
|
setGroupPlanToChangeToCode,
|
|
setGroupPlanToChangeToSize,
|
|
setGroupPlanToChangeToUsage,
|
|
setModalIdShown,
|
|
setPlanCodeToChangeTo,
|
|
setRecurlyLoadError,
|
|
showCancellation,
|
|
setShowCancellation,
|
|
leavingGroupId,
|
|
setLeavingGroupId,
|
|
]
|
|
)
|
|
|
|
return (
|
|
<SubscriptionDashboardContext.Provider value={value}>
|
|
{children}
|
|
</SubscriptionDashboardContext.Provider>
|
|
)
|
|
}
|
|
|
|
export function useSubscriptionDashboardContext() {
|
|
const context = useContext(SubscriptionDashboardContext)
|
|
if (!context) {
|
|
throw new Error(
|
|
'SubscriptionDashboardContext is only available inside SubscriptionDashboardProvider'
|
|
)
|
|
}
|
|
return context
|
|
}
|