From 6833dae879bfcfafd65f61bc0bd5adfabf5b53cb Mon Sep 17 00:00:00 2001 From: Jessica Lawshe Date: Tue, 7 Feb 2023 09:38:30 -0600 Subject: [PATCH] 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 --- .../states/active/change-plan/change-plan.tsx | 32 +++++++---- .../change-plan/individual-plans-table.tsx | 8 ++- .../subscription-dashboard-context.tsx | 42 +++++++++++++-- .../subscription/util/is-recurly-loaded.tsx | 3 ++ .../stylesheets/components/tables.less | 6 +++ .../dashboard/states/active/active.test.tsx | 4 +- .../active/change-plan/change-plan.test.tsx | 54 +++++++++++-------- .../render-with-subscription-dash-context.tsx | 40 +++++++++++++- services/web/types/subscription/plan.ts | 1 + 9 files changed, 150 insertions(+), 40 deletions(-) create mode 100644 services/web/frontend/js/features/subscription/util/is-recurly-loaded.tsx diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/change-plan.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/change-plan.tsx index 2dbda78ae5..0316d4de42 100644 --- a/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/change-plan.tsx +++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/change-plan.tsx @@ -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 ( - <> -

{t('change_plan')}

- - - - ) + if (queryingIndividualPlansData) { + return ( + <> +

{t('change_plan')}

+ + + ) + } else { + return ( + <> +

{t('change_plan')}

+ + + + ) + } } diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/individual-plans-table.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/individual-plans-table.tsx index 68ff59beeb..dac9f40f6b 100644 --- a/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/individual-plans-table.tsx +++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/individual-plans-table.tsx @@ -74,7 +74,7 @@ function PlansRow({ plan }: { plan: Plan }) { {plan.name} - {/* todo: {{ displayPrice }} */}/ {plan.annual ? t('year') : t('month')} + {plan.displayPrice} / {plan.annual ? t('year') : t('month')} @@ -96,9 +96,13 @@ function PlansRows({ plans }: { plans: Array }) { export function IndividualPlansTable({ plans }: { plans: Array }) { const { t } = useTranslation() + const { recurlyLoadError, showChangePersonalPlan } = + useSubscriptionDashboardContext() + + if (!showChangePersonalPlan || !plans || recurlyLoadError) return null return ( - +
diff --git a/services/web/frontend/js/features/subscription/context/subscription-dashboard-context.tsx b/services/web/frontend/js/features/subscription/context/subscription-dashboard-context.tsx index 297950c665..8611de86a5 100644 --- a/services/web/frontend/js/features/subscription/context/subscription-dashboard-context.tsx +++ b/services/web/frontend/js/features/subscription/context/subscription-dashboard-context.tsx @@ -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 personalSubscription?: Subscription plans: Array + queryingIndividualPlansData: boolean recurlyLoadError: boolean setRecurlyLoadError: React.Dispatch> 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( () => ({ @@ -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, diff --git a/services/web/frontend/js/features/subscription/util/is-recurly-loaded.tsx b/services/web/frontend/js/features/subscription/util/is-recurly-loaded.tsx new file mode 100644 index 0000000000..500af34d31 --- /dev/null +++ b/services/web/frontend/js/features/subscription/util/is-recurly-loaded.tsx @@ -0,0 +1,3 @@ +export function isRecurlyLoaded() { + return typeof recurly !== 'undefined' +} diff --git a/services/web/frontend/stylesheets/components/tables.less b/services/web/frontend/stylesheets/components/tables.less index b839468075..d88ba303c8 100755 --- a/services/web/frontend/stylesheets/components/tables.less +++ b/services/web/frontend/stylesheets/components/tables.less @@ -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 { diff --git a/services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx index 20734a4ed7..d46ff04f82 100644 --- a/services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx +++ b/services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx @@ -79,7 +79,7 @@ describe('', 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('', 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 diff --git a/services/web/test/frontend/features/subscription/components/dashboard/states/active/change-plan/change-plan.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/states/active/change-plan/change-plan.test.tsx index f988490e0b..70190af80f 100644 --- a/services/web/test/frontend/features/subscription/components/dashboard/states/active/change-plan/change-plan.test.tsx +++ b/services/web/test/frontend/features/subscription/components/dashboard/states/active/change-plan/change-plan.test.tsx @@ -30,13 +30,17 @@ describe('', 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( , { metaTags: [ { name: 'ol-subscription', value: annualActiveSubscription }, plansMetaTag, + { + name: 'ol-recommendedCurrency', + value: 'USD', + }, ], } ) @@ -44,6 +48,8 @@ describe('', 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('', 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( - , - { - 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( , { @@ -88,7 +81,7 @@ describe('', 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('', function () { ) expect(container).not.to.be.null }) + + it('shows a loading message while still querying Recurly for prices', function () { + renderWithSubscriptionDashContext( + , + { + metaTags: [ + { name: 'ol-subscription', value: pendingSubscriptionChange }, + plansMetaTag, + ], + } + ) + + const button = screen.getByRole('button', { name: 'Change plan' }) + fireEvent.click(button) + + screen.findByText('Loading', { exact: false }) + }) }) diff --git a/services/web/test/frontend/features/subscription/helpers/render-with-subscription-dash-context.tsx b/services/web/test/frontend/features/subscription/helpers/render-with-subscription-dash-context.tsx index 62a6ea27d7..122c30f89b 100644 --- a/services/web/test/frontend/features/subscription/helpers/render-with-subscription-dash-context.tsx +++ b/services/web/test/frontend/features/subscription/helpers/render-with-subscription-dash-context.tsx @@ -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 }[] 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() } diff --git a/services/web/types/subscription/plan.ts b/services/web/types/subscription/plan.ts index eecf9bed79..fec27595de 100644 --- a/services/web/types/subscription/plan.ts +++ b/services/web/types/subscription/plan.ts @@ -17,6 +17,7 @@ type Features = { export type Plan = { annual?: boolean + displayPrice?: string featureDescription?: Record[] features?: Features groupPlan?: boolean
{t('name')}