mirror of
https://github.com/overleaf/overleaf.git
synced 2025-04-11 17:15:35 +00:00
Merge pull request #11619 from overleaf/jel-react-personal-subscription-dash-change-plan
[web] Query display price data for change plan UI in React subscription dash GitOrigin-RevId: e05779d1f289ee55a1b8be17b182905fca66e44f
This commit is contained in:
parent
d4057a7bcc
commit
f3d055af86
9 changed files with 150 additions and 40 deletions
services/web
frontend
js/features/subscription
components/dashboard/states/active/change-plan
context
util
stylesheets/components
test/frontend/features/subscription
components/dashboard/states/active
helpers
types/subscription
|
@ -1,20 +1,34 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import LoadingSpinner from '../../../../../../../shared/components/loading-spinner'
|
||||
import { useSubscriptionDashboardContext } from '../../../../../context/subscription-dashboard-context'
|
||||
import { ChangeToGroupPlan } from './change-to-group-plan'
|
||||
import { IndividualPlansTable } from './individual-plans-table'
|
||||
|
||||
export function ChangePlan() {
|
||||
const { t } = useTranslation()
|
||||
const { plans, recurlyLoadError, showChangePersonalPlan } =
|
||||
useSubscriptionDashboardContext()
|
||||
const {
|
||||
plans,
|
||||
queryingIndividualPlansData,
|
||||
recurlyLoadError,
|
||||
showChangePersonalPlan,
|
||||
} = useSubscriptionDashboardContext()
|
||||
|
||||
if (!showChangePersonalPlan || !plans || recurlyLoadError) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2>{t('change_plan')}</h2>
|
||||
<IndividualPlansTable plans={plans} />
|
||||
<ChangeToGroupPlan />
|
||||
</>
|
||||
)
|
||||
if (queryingIndividualPlansData) {
|
||||
return (
|
||||
<>
|
||||
<h2>{t('change_plan')}</h2>
|
||||
<LoadingSpinner />
|
||||
</>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<h2>{t('change_plan')}</h2>
|
||||
<IndividualPlansTable plans={plans} />
|
||||
<ChangeToGroupPlan />
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -74,7 +74,7 @@ function PlansRow({ plan }: { plan: Plan }) {
|
|||
<strong>{plan.name}</strong>
|
||||
</td>
|
||||
<td>
|
||||
{/* todo: {{ displayPrice }} */}/ {plan.annual ? t('year') : t('month')}
|
||||
{plan.displayPrice} / {plan.annual ? t('year') : t('month')}
|
||||
</td>
|
||||
<td>
|
||||
<ChangePlanButton plan={plan} />
|
||||
|
@ -96,9 +96,13 @@ function PlansRows({ plans }: { plans: Array<Plan> }) {
|
|||
|
||||
export function IndividualPlansTable({ plans }: { plans: Array<Plan> }) {
|
||||
const { t } = useTranslation()
|
||||
const { recurlyLoadError, showChangePersonalPlan } =
|
||||
useSubscriptionDashboardContext()
|
||||
|
||||
if (!showChangePersonalPlan || !plans || recurlyLoadError) return null
|
||||
|
||||
return (
|
||||
<table className="table">
|
||||
<table className="table table-vertically-centered-cells">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('name')}</th>
|
||||
|
|
|
@ -13,6 +13,8 @@ import {
|
|||
import { Plan } from '../../../../../types/subscription/plan'
|
||||
import { Institution } from '../../../../../types/institution'
|
||||
import getMeta from '../../../utils/meta'
|
||||
import { loadDisplayPriceWithTaxPromise } from '../util/recurly-pricing'
|
||||
import { isRecurlyLoaded } from '../util/is-recurly-loaded'
|
||||
|
||||
type SubscriptionDashboardContextValue = {
|
||||
hasDisplayedSubscription: boolean
|
||||
|
@ -20,6 +22,7 @@ type SubscriptionDashboardContextValue = {
|
|||
managedGroupSubscriptions: Array<ManagedGroupSubscription>
|
||||
personalSubscription?: Subscription
|
||||
plans: Array<Plan>
|
||||
queryingIndividualPlansData: boolean
|
||||
recurlyLoadError: boolean
|
||||
setRecurlyLoadError: React.Dispatch<React.SetStateAction<boolean>>
|
||||
showCancellation: boolean
|
||||
|
@ -40,11 +43,15 @@ export function SubscriptionDashboardProvider({
|
|||
const [recurlyLoadError, setRecurlyLoadError] = useState(false)
|
||||
const [showCancellation, setShowCancellation] = useState(false)
|
||||
const [showChangePersonalPlan, setShowChangePersonalPlan] = useState(false)
|
||||
const [plans, setPlans] = useState([])
|
||||
const [queryingIndividualPlansData, setQueryingIndividualPlansData] =
|
||||
useState(true)
|
||||
|
||||
const plans = getMeta('ol-plans')
|
||||
const plansWithoutDisplayPrice = getMeta('ol-plans')
|
||||
const institutionMemberships = getMeta('ol-currentInstitutionsWithLicence')
|
||||
const personalSubscription = getMeta('ol-subscription')
|
||||
const managedGroupSubscriptions = getMeta('ol-managedGroupSubscriptions')
|
||||
const recurlyApiKey = getMeta('ol-recurlyApiKey')
|
||||
|
||||
const hasDisplayedSubscription =
|
||||
institutionMemberships?.length > 0 ||
|
||||
|
@ -52,10 +59,37 @@ export function SubscriptionDashboardProvider({
|
|||
managedGroupSubscriptions
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window.recurly === 'undefined' || !window.recurly) {
|
||||
if (!isRecurlyLoaded()) {
|
||||
setRecurlyLoadError(true)
|
||||
} else {
|
||||
recurly.configure(recurlyApiKey)
|
||||
}
|
||||
}, [setRecurlyLoadError])
|
||||
}, [recurlyApiKey, setRecurlyLoadError])
|
||||
|
||||
useEffect(() => {
|
||||
if (isRecurlyLoaded() && plansWithoutDisplayPrice && personalSubscription) {
|
||||
const { currency, taxRate } = personalSubscription.recurly
|
||||
const fetchPlansDisplayPrices = async () => {
|
||||
for (const plan of plansWithoutDisplayPrice) {
|
||||
try {
|
||||
const priceData = await loadDisplayPriceWithTaxPromise(
|
||||
plan.planCode,
|
||||
currency,
|
||||
taxRate
|
||||
)
|
||||
if (priceData?.totalForDisplay) {
|
||||
plan.displayPrice = priceData.totalForDisplay
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
setPlans(plansWithoutDisplayPrice)
|
||||
setQueryingIndividualPlansData(false)
|
||||
}
|
||||
fetchPlansDisplayPrices().catch(console.error)
|
||||
}
|
||||
}, [personalSubscription, plansWithoutDisplayPrice])
|
||||
|
||||
const value = useMemo<SubscriptionDashboardContextValue>(
|
||||
() => ({
|
||||
|
@ -64,6 +98,7 @@ export function SubscriptionDashboardProvider({
|
|||
managedGroupSubscriptions,
|
||||
personalSubscription,
|
||||
plans,
|
||||
queryingIndividualPlansData,
|
||||
recurlyLoadError,
|
||||
setRecurlyLoadError,
|
||||
showCancellation,
|
||||
|
@ -77,6 +112,7 @@ export function SubscriptionDashboardProvider({
|
|||
managedGroupSubscriptions,
|
||||
personalSubscription,
|
||||
plans,
|
||||
queryingIndividualPlansData,
|
||||
recurlyLoadError,
|
||||
setRecurlyLoadError,
|
||||
showCancellation,
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
export function isRecurlyLoaded() {
|
||||
return typeof recurly !== 'undefined'
|
||||
}
|
|
@ -62,6 +62,12 @@ th {
|
|||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.table-vertically-centered-cells {
|
||||
> tbody > tr > td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
// Condensed table w/ half padding
|
||||
|
||||
.table-condensed {
|
||||
|
|
|
@ -79,7 +79,7 @@ describe('<ActiveSubscription />', function () {
|
|||
expectedInActiveSubscription(annualActiveSubscription)
|
||||
})
|
||||
|
||||
it('shows change plan UI when button clicked', function () {
|
||||
it('shows change plan UI when button clicked', async function () {
|
||||
renderActiveSubscription(annualActiveSubscription)
|
||||
|
||||
const button = screen.getByRole('button', { name: 'Change plan' })
|
||||
|
@ -88,7 +88,7 @@ describe('<ActiveSubscription />', function () {
|
|||
// confirm main dash UI still shown
|
||||
screen.getByText('You are currently subscribed to the', { exact: false })
|
||||
|
||||
screen.getByRole('heading', { name: 'Change plan' })
|
||||
await screen.findByRole('heading', { name: 'Change plan' })
|
||||
expect(
|
||||
screen.getAllByRole('button', { name: 'Change to this plan' }).length > 0
|
||||
).to.be.true
|
||||
|
|
|
@ -30,13 +30,17 @@ describe('<ChangePlan />', function () {
|
|||
expect(container.firstChild).to.be.null
|
||||
})
|
||||
|
||||
it('renders the individual plans table', function () {
|
||||
it('renders the individual plans table and group plans UI', async function () {
|
||||
renderWithSubscriptionDashContext(
|
||||
<ActiveSubscription subscription={annualActiveSubscription} />,
|
||||
{
|
||||
metaTags: [
|
||||
{ name: 'ol-subscription', value: annualActiveSubscription },
|
||||
plansMetaTag,
|
||||
{
|
||||
name: 'ol-recommendedCurrency',
|
||||
value: 'USD',
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
@ -44,6 +48,8 @@ describe('<ChangePlan />', function () {
|
|||
const button = screen.getByRole('button', { name: 'Change plan' })
|
||||
fireEvent.click(button)
|
||||
|
||||
await screen.findByText('Looking for multiple licenses?')
|
||||
|
||||
const changeToPlanButtons = screen.queryAllByRole('button', {
|
||||
name: 'Change to this plan',
|
||||
})
|
||||
|
@ -51,30 +57,17 @@ describe('<ChangePlan />', function () {
|
|||
screen.getByText('Your plan')
|
||||
|
||||
const annualPlans = plans.filter(plan => plan.annual)
|
||||
expect(screen.getAllByText('/ year').length).to.equal(annualPlans.length)
|
||||
expect(screen.getAllByText('/ month').length).to.equal(
|
||||
expect(screen.getAllByText('/ year', { exact: false }).length).to.equal(
|
||||
annualPlans.length
|
||||
)
|
||||
expect(screen.getAllByText('/ month', { exact: false }).length).to.equal(
|
||||
plans.length - annualPlans.length
|
||||
)
|
||||
|
||||
expect(screen.queryByText('loading', { exact: false })).to.be.null
|
||||
})
|
||||
|
||||
it('renders the change to group plan UI', function () {
|
||||
renderWithSubscriptionDashContext(
|
||||
<ActiveSubscription subscription={annualActiveSubscription} />,
|
||||
{
|
||||
metaTags: [
|
||||
{ name: 'ol-subscription', value: annualActiveSubscription },
|
||||
plansMetaTag,
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
const button = screen.getByRole('button', { name: 'Change plan' })
|
||||
fireEvent.click(button)
|
||||
|
||||
screen.getByText('Looking for multiple licenses?')
|
||||
})
|
||||
|
||||
it('renders "Your new plan" and "Keep current plan" when there is a pending plan change', function () {
|
||||
it('renders "Your new plan" and "Keep current plan" when there is a pending plan change', async function () {
|
||||
renderWithSubscriptionDashContext(
|
||||
<ActiveSubscription subscription={pendingSubscriptionChange} />,
|
||||
{
|
||||
|
@ -88,7 +81,7 @@ describe('<ChangePlan />', function () {
|
|||
const button = screen.getByRole('button', { name: 'Change plan' })
|
||||
fireEvent.click(button)
|
||||
|
||||
screen.getByText('Your new plan')
|
||||
await screen.findByText('Your new plan')
|
||||
screen.getByRole('button', { name: 'Keep my current plan' })
|
||||
})
|
||||
|
||||
|
@ -104,4 +97,21 @@ describe('<ChangePlan />', function () {
|
|||
)
|
||||
expect(container).not.to.be.null
|
||||
})
|
||||
|
||||
it('shows a loading message while still querying Recurly for prices', function () {
|
||||
renderWithSubscriptionDashContext(
|
||||
<ActiveSubscription subscription={pendingSubscriptionChange} />,
|
||||
{
|
||||
metaTags: [
|
||||
{ name: 'ol-subscription', value: pendingSubscriptionChange },
|
||||
plansMetaTag,
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
const button = screen.getByRole('button', { name: 'Change plan' })
|
||||
fireEvent.click(button)
|
||||
|
||||
screen.findByText('Loading', { exact: false })
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import { render } from '@testing-library/react'
|
||||
import { SubscriptionDashboardProvider } from '../../../../../frontend/js/features/subscription/context/subscription-dashboard-context'
|
||||
import { plans } from '../fixtures/plans'
|
||||
|
||||
export function renderWithSubscriptionDashContext(
|
||||
component: React.ReactElement,
|
||||
options?: {
|
||||
metaTags?: { name: string; value: string | object | Array<object> }[]
|
||||
recurlyNotLoaded?: boolean
|
||||
queryingRecurly?: boolean
|
||||
}
|
||||
) {
|
||||
const SubscriptionDashboardProviderWrapper = ({
|
||||
|
@ -23,7 +25,41 @@ export function renderWithSubscriptionDashContext(
|
|||
|
||||
if (!options?.recurlyNotLoaded) {
|
||||
// @ts-ignore
|
||||
window.recurly = {}
|
||||
global.recurly = {
|
||||
configure: () => {},
|
||||
Pricing: {
|
||||
Subscription: () => {
|
||||
return {
|
||||
plan: (planCode: string) => {
|
||||
const plan = plans.find(p => p.planCode === planCode)
|
||||
|
||||
const response = {
|
||||
next: {
|
||||
total: plan?.price_in_cents
|
||||
? plan.price_in_cents / 100
|
||||
: undefined,
|
||||
},
|
||||
}
|
||||
return {
|
||||
currency: () => {
|
||||
return {
|
||||
catch: () => {
|
||||
return {
|
||||
done: (callback: (response: object) => void) => {
|
||||
if (!options?.queryingRecurly) {
|
||||
return callback(response)
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return render(component, {
|
||||
|
@ -33,6 +69,6 @@ export function renderWithSubscriptionDashContext(
|
|||
|
||||
export function cleanUpContext() {
|
||||
// @ts-ignore
|
||||
delete window.recurly
|
||||
delete global.recurly
|
||||
window.metaAttributesCache = new Map()
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ type Features = {
|
|||
|
||||
export type Plan = {
|
||||
annual?: boolean
|
||||
displayPrice?: string
|
||||
featureDescription?: Record<string, unknown>[]
|
||||
features?: Features
|
||||
groupPlan?: boolean
|
||||
|
|
Loading…
Add table
Reference in a new issue