Преглед изворни кода

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
Jessica Lawshe пре 3 година
родитељ
комит
f3d055af86

+ 23 - 9
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 (
-    <>
-      <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 />
+      </>
+    )
+  }
 }

+ 6 - 2
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 }) {
         <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>

+ 39 - 3
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<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,

+ 3 - 0
services/web/frontend/js/features/subscription/util/is-recurly-loaded.tsx

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

+ 6 - 0
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 {

+ 2 - 2
services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx

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

+ 31 - 21
services/web/test/frontend/features/subscription/components/dashboard/states/active/change-plan/change-plan.test.tsx

@@ -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(
-      plans.length - annualPlans.length
+    expect(screen.getAllByText('/ year', { exact: false }).length).to.equal(
+      annualPlans.length
     )
-  })
-
-  it('renders the change to group plan UI', function () {
-    renderWithSubscriptionDashContext(
-      <ActiveSubscription subscription={annualActiveSubscription} />,
-      {
-        metaTags: [
-          { name: 'ol-subscription', value: annualActiveSubscription },
-          plansMetaTag,
-        ],
-      }
+    expect(screen.getAllByText('/ month', { exact: false }).length).to.equal(
+      plans.length - annualPlans.length
     )
 
-    const button = screen.getByRole('button', { name: 'Change plan' })
-    fireEvent.click(button)
-
-    screen.getByText('Looking for multiple licenses?')
+    expect(screen.queryByText('loading', { exact: false })).to.be.null
   })
 
-  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 })
+  })
 })

+ 38 - 2
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<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()
 }

+ 1 - 0
services/web/types/subscription/plan.ts

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