1
0
Fork 0
mirror of https://github.com/overleaf/overleaf.git synced 2025-04-11 17:15:35 +00:00

Merge pull request 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-07 09:38:30 -06:00 committed by Copybot
parent d4057a7bcc
commit f3d055af86
9 changed files with 150 additions and 40 deletions
services/web
frontend
js/features/subscription
stylesheets/components
test/frontend/features/subscription
components/dashboard/states/active
helpers
types/subscription

View file

@ -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 />
</>
)
}
}

View file

@ -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>

View file

@ -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,

View file

@ -0,0 +1,3 @@
export function isRecurlyLoaded() {
return typeof recurly !== 'undefined'
}

View file

@ -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 {

View file

@ -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

View file

@ -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 })
})
})

View file

@ -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()
}

View file

@ -17,6 +17,7 @@ type Features = {
export type Plan = {
annual?: boolean
displayPrice?: string
featureDescription?: Record<string, unknown>[]
features?: Features
groupPlan?: boolean