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:
Jessica Lawshe
2023-02-08 09:06:44 +00:00
committed by Copybot
parent 40c172a914
commit 6833dae879
9 changed files with 150 additions and 40 deletions
@@ -1,20 +1,34 @@
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import LoadingSpinner from '../../../../../../../shared/components/loading-spinner'
import { useSubscriptionDashboardContext } from '../../../../../context/subscription-dashboard-context' import { useSubscriptionDashboardContext } from '../../../../../context/subscription-dashboard-context'
import { ChangeToGroupPlan } from './change-to-group-plan' import { ChangeToGroupPlan } from './change-to-group-plan'
import { IndividualPlansTable } from './individual-plans-table' import { IndividualPlansTable } from './individual-plans-table'
export function ChangePlan() { export function ChangePlan() {
const { t } = useTranslation() const { t } = useTranslation()
const { plans, recurlyLoadError, showChangePersonalPlan } = const {
useSubscriptionDashboardContext() plans,
queryingIndividualPlansData,
recurlyLoadError,
showChangePersonalPlan,
} = useSubscriptionDashboardContext()
if (!showChangePersonalPlan || !plans || recurlyLoadError) return null if (!showChangePersonalPlan || !plans || recurlyLoadError) return null
return ( if (queryingIndividualPlansData) {
<> return (
<h2>{t('change_plan')}</h2> <>
<IndividualPlansTable plans={plans} /> <h2>{t('change_plan')}</h2>
<ChangeToGroupPlan /> <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> <strong>{plan.name}</strong>
</td> </td>
<td> <td>
{/* todo: {{ displayPrice }} */}/ {plan.annual ? t('year') : t('month')} {plan.displayPrice} / {plan.annual ? t('year') : t('month')}
</td> </td>
<td> <td>
<ChangePlanButton plan={plan} /> <ChangePlanButton plan={plan} />
@@ -96,9 +96,13 @@ function PlansRows({ plans }: { plans: Array<Plan> }) {
export function IndividualPlansTable({ plans }: { plans: Array<Plan> }) { export function IndividualPlansTable({ plans }: { plans: Array<Plan> }) {
const { t } = useTranslation() const { t } = useTranslation()
const { recurlyLoadError, showChangePersonalPlan } =
useSubscriptionDashboardContext()
if (!showChangePersonalPlan || !plans || recurlyLoadError) return null
return ( return (
<table className="table"> <table className="table table-vertically-centered-cells">
<thead> <thead>
<tr> <tr>
<th>{t('name')}</th> <th>{t('name')}</th>
@@ -13,6 +13,8 @@ import {
import { Plan } from '../../../../../types/subscription/plan' import { Plan } from '../../../../../types/subscription/plan'
import { Institution } from '../../../../../types/institution' import { Institution } from '../../../../../types/institution'
import getMeta from '../../../utils/meta' import getMeta from '../../../utils/meta'
import { loadDisplayPriceWithTaxPromise } from '../util/recurly-pricing'
import { isRecurlyLoaded } from '../util/is-recurly-loaded'
type SubscriptionDashboardContextValue = { type SubscriptionDashboardContextValue = {
hasDisplayedSubscription: boolean hasDisplayedSubscription: boolean
@@ -20,6 +22,7 @@ type SubscriptionDashboardContextValue = {
managedGroupSubscriptions: Array<ManagedGroupSubscription> managedGroupSubscriptions: Array<ManagedGroupSubscription>
personalSubscription?: Subscription personalSubscription?: Subscription
plans: Array<Plan> plans: Array<Plan>
queryingIndividualPlansData: boolean
recurlyLoadError: boolean recurlyLoadError: boolean
setRecurlyLoadError: React.Dispatch<React.SetStateAction<boolean>> setRecurlyLoadError: React.Dispatch<React.SetStateAction<boolean>>
showCancellation: boolean showCancellation: boolean
@@ -40,11 +43,15 @@ export function SubscriptionDashboardProvider({
const [recurlyLoadError, setRecurlyLoadError] = useState(false) const [recurlyLoadError, setRecurlyLoadError] = useState(false)
const [showCancellation, setShowCancellation] = useState(false) const [showCancellation, setShowCancellation] = useState(false)
const [showChangePersonalPlan, setShowChangePersonalPlan] = 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 institutionMemberships = getMeta('ol-currentInstitutionsWithLicence')
const personalSubscription = getMeta('ol-subscription') const personalSubscription = getMeta('ol-subscription')
const managedGroupSubscriptions = getMeta('ol-managedGroupSubscriptions') const managedGroupSubscriptions = getMeta('ol-managedGroupSubscriptions')
const recurlyApiKey = getMeta('ol-recurlyApiKey')
const hasDisplayedSubscription = const hasDisplayedSubscription =
institutionMemberships?.length > 0 || institutionMemberships?.length > 0 ||
@@ -52,10 +59,37 @@ export function SubscriptionDashboardProvider({
managedGroupSubscriptions managedGroupSubscriptions
useEffect(() => { useEffect(() => {
if (typeof window.recurly === 'undefined' || !window.recurly) { if (!isRecurlyLoaded()) {
setRecurlyLoadError(true) 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>( const value = useMemo<SubscriptionDashboardContextValue>(
() => ({ () => ({
@@ -64,6 +98,7 @@ export function SubscriptionDashboardProvider({
managedGroupSubscriptions, managedGroupSubscriptions,
personalSubscription, personalSubscription,
plans, plans,
queryingIndividualPlansData,
recurlyLoadError, recurlyLoadError,
setRecurlyLoadError, setRecurlyLoadError,
showCancellation, showCancellation,
@@ -77,6 +112,7 @@ export function SubscriptionDashboardProvider({
managedGroupSubscriptions, managedGroupSubscriptions,
personalSubscription, personalSubscription,
plans, plans,
queryingIndividualPlansData,
recurlyLoadError, recurlyLoadError,
setRecurlyLoadError, setRecurlyLoadError,
showCancellation, showCancellation,
@@ -0,0 +1,3 @@
export function isRecurlyLoaded() {
return typeof recurly !== 'undefined'
}
@@ -62,6 +62,12 @@ th {
word-wrap: break-word; word-wrap: break-word;
} }
.table-vertically-centered-cells {
> tbody > tr > td {
vertical-align: middle;
}
}
// Condensed table w/ half padding // Condensed table w/ half padding
.table-condensed { .table-condensed {
@@ -79,7 +79,7 @@ describe('<ActiveSubscription />', function () {
expectedInActiveSubscription(annualActiveSubscription) expectedInActiveSubscription(annualActiveSubscription)
}) })
it('shows change plan UI when button clicked', function () { it('shows change plan UI when button clicked', async function () {
renderActiveSubscription(annualActiveSubscription) renderActiveSubscription(annualActiveSubscription)
const button = screen.getByRole('button', { name: 'Change plan' }) const button = screen.getByRole('button', { name: 'Change plan' })
@@ -88,7 +88,7 @@ describe('<ActiveSubscription />', function () {
// confirm main dash UI still shown // confirm main dash UI still shown
screen.getByText('You are currently subscribed to the', { exact: false }) screen.getByText('You are currently subscribed to the', { exact: false })
screen.getByRole('heading', { name: 'Change plan' }) await screen.findByRole('heading', { name: 'Change plan' })
expect( expect(
screen.getAllByRole('button', { name: 'Change to this plan' }).length > 0 screen.getAllByRole('button', { name: 'Change to this plan' }).length > 0
).to.be.true ).to.be.true
@@ -30,13 +30,17 @@ describe('<ChangePlan />', function () {
expect(container.firstChild).to.be.null 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( renderWithSubscriptionDashContext(
<ActiveSubscription subscription={annualActiveSubscription} />, <ActiveSubscription subscription={annualActiveSubscription} />,
{ {
metaTags: [ metaTags: [
{ name: 'ol-subscription', value: annualActiveSubscription }, { name: 'ol-subscription', value: annualActiveSubscription },
plansMetaTag, plansMetaTag,
{
name: 'ol-recommendedCurrency',
value: 'USD',
},
], ],
} }
) )
@@ -44,6 +48,8 @@ describe('<ChangePlan />', function () {
const button = screen.getByRole('button', { name: 'Change plan' }) const button = screen.getByRole('button', { name: 'Change plan' })
fireEvent.click(button) fireEvent.click(button)
await screen.findByText('Looking for multiple licenses?')
const changeToPlanButtons = screen.queryAllByRole('button', { const changeToPlanButtons = screen.queryAllByRole('button', {
name: 'Change to this plan', name: 'Change to this plan',
}) })
@@ -51,30 +57,17 @@ describe('<ChangePlan />', function () {
screen.getByText('Your plan') screen.getByText('Your plan')
const annualPlans = plans.filter(plan => plan.annual) const annualPlans = plans.filter(plan => plan.annual)
expect(screen.getAllByText('/ year').length).to.equal(annualPlans.length) expect(screen.getAllByText('/ year', { exact: false }).length).to.equal(
expect(screen.getAllByText('/ month').length).to.equal( annualPlans.length
)
expect(screen.getAllByText('/ month', { exact: false }).length).to.equal(
plans.length - annualPlans.length plans.length - annualPlans.length
) )
expect(screen.queryByText('loading', { exact: false })).to.be.null
}) })
it('renders the change to group plan UI', function () { it('renders "Your new plan" and "Keep current plan" when there is a pending plan change', async 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 () {
renderWithSubscriptionDashContext( renderWithSubscriptionDashContext(
<ActiveSubscription subscription={pendingSubscriptionChange} />, <ActiveSubscription subscription={pendingSubscriptionChange} />,
{ {
@@ -88,7 +81,7 @@ describe('<ChangePlan />', function () {
const button = screen.getByRole('button', { name: 'Change plan' }) const button = screen.getByRole('button', { name: 'Change plan' })
fireEvent.click(button) fireEvent.click(button)
screen.getByText('Your new plan') await screen.findByText('Your new plan')
screen.getByRole('button', { name: 'Keep my current plan' }) screen.getByRole('button', { name: 'Keep my current plan' })
}) })
@@ -104,4 +97,21 @@ describe('<ChangePlan />', function () {
) )
expect(container).not.to.be.null 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 { render } from '@testing-library/react'
import { SubscriptionDashboardProvider } from '../../../../../frontend/js/features/subscription/context/subscription-dashboard-context' import { SubscriptionDashboardProvider } from '../../../../../frontend/js/features/subscription/context/subscription-dashboard-context'
import { plans } from '../fixtures/plans'
export function renderWithSubscriptionDashContext( export function renderWithSubscriptionDashContext(
component: React.ReactElement, component: React.ReactElement,
options?: { options?: {
metaTags?: { name: string; value: string | object | Array<object> }[] metaTags?: { name: string; value: string | object | Array<object> }[]
recurlyNotLoaded?: boolean recurlyNotLoaded?: boolean
queryingRecurly?: boolean
} }
) { ) {
const SubscriptionDashboardProviderWrapper = ({ const SubscriptionDashboardProviderWrapper = ({
@@ -23,7 +25,41 @@ export function renderWithSubscriptionDashContext(
if (!options?.recurlyNotLoaded) { if (!options?.recurlyNotLoaded) {
// @ts-ignore // @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, { return render(component, {
@@ -33,6 +69,6 @@ export function renderWithSubscriptionDashContext(
export function cleanUpContext() { export function cleanUpContext() {
// @ts-ignore // @ts-ignore
delete window.recurly delete global.recurly
window.metaAttributesCache = new Map() window.metaAttributesCache = new Map()
} }
+1
View File
@@ -17,6 +17,7 @@ type Features = {
export type Plan = { export type Plan = {
annual?: boolean annual?: boolean
displayPrice?: string
featureDescription?: Record<string, unknown>[] featureDescription?: Record<string, unknown>[]
features?: Features features?: Features
groupPlan?: boolean groupPlan?: boolean