Procházet zdrojové kódy

Merge pull request #11919 from overleaf/ii-react-subscription-dash-user-email-not-matching-recurly-email

[web] Subscription dash user email not matching recurly email section - react migration

GitOrigin-RevId: 8d89dbc804694afffea2a92cebdeb5d4e9389810
ilkin-overleaf před 3 roky
rodič
revize
9f95c7c2ab

+ 3 - 0
services/web/frontend/extracted-translations.json

@@ -636,6 +636,8 @@
   "recompile_from_scratch": "",
   "recompile_pdf": "",
   "reconnect": "",
+  "recurly_email_update_needed": "",
+  "recurly_email_updated": "",
   "redirect_to_editor": "",
   "reduce_costs_group_licenses": "",
   "reference_error_relink_hint": "",
@@ -876,6 +878,7 @@
   "update_account_info": "",
   "update_dropbox_settings": "",
   "update_your_billing_details": "",
+  "updating": "",
   "upgrade": "",
   "upgrade_cc_btn": "",
   "upgrade_for_longer_compiles": "",

+ 25 - 25
services/web/frontend/js/features/subscription/components/dashboard/institution-memberships.tsx

@@ -20,32 +20,32 @@ function InstitutionMemberships() {
     )
   }
 
+  if (!institutionMemberships.length) return null
+
   return (
-    <>
-      <div>
-        {institutionMemberships.map((institution: Institution) => (
-          <div key={`${institution.id}`}>
-            <Trans
-              i18nKey="you_are_on_x_plan_as_a_confirmed_member_of_institution_y"
-              values={{
-                planName: 'Professional',
-                institutionName: institution.name || '',
-              }}
-              components={[
-                // eslint-disable-next-line react/jsx-key, jsx-a11y/anchor-has-content
-                <a href="/user/subscription/plans" rel="noopener" />,
-                // eslint-disable-next-line react/jsx-key
-                <strong />,
-                // eslint-disable-next-line react/jsx-key
-                <strong />,
-              ]}
-            />
-            <hr />
-          </div>
-        ))}
-        {institutionMemberships.length > 0 && <PremiumFeaturesLink />}
-      </div>
-    </>
+    <div>
+      {institutionMemberships.map((institution: Institution) => (
+        <div key={`${institution.id}`}>
+          <Trans
+            i18nKey="you_are_on_x_plan_as_a_confirmed_member_of_institution_y"
+            values={{
+              planName: 'Professional',
+              institutionName: institution.name || '',
+            }}
+            components={[
+              // eslint-disable-next-line react/jsx-key, jsx-a11y/anchor-has-content
+              <a href="/user/subscription/plans" rel="noopener" />,
+              // eslint-disable-next-line react/jsx-key
+              <strong />,
+              // eslint-disable-next-line react/jsx-key
+              <strong />,
+            ]}
+          />
+          <hr />
+        </div>
+      ))}
+      <PremiumFeaturesLink />
+    </div>
   )
 }
 

+ 58 - 0
services/web/frontend/js/features/subscription/components/dashboard/personal-subscription-recurly-sync-email.tsx

@@ -0,0 +1,58 @@
+import { useTranslation, Trans } from 'react-i18next'
+import { useSubscriptionDashboardContext } from '../../context/subscription-dashboard-context'
+import { FormGroup, Alert } from 'react-bootstrap'
+import getMeta from '../../../../utils/meta'
+import useAsync from '../../../../shared/hooks/use-async'
+import { postJSON } from '../../../../infrastructure/fetch-json'
+
+function PersonalSubscriptionRecurlySyncEmail() {
+  const { t } = useTranslation()
+  const { personalSubscription } = useSubscriptionDashboardContext()
+  const userEmail = getMeta('ol-usersEmail') as string
+  const { isLoading, isSuccess, runAsync } = useAsync()
+
+  const handleSubmit = (e: React.FormEvent) => {
+    e.preventDefault()
+    runAsync(postJSON('/user/subscription/account/email'))
+  }
+
+  if (!personalSubscription || !('recurly' in personalSubscription)) return null
+
+  const recurlyEmail = personalSubscription.recurly.account.email
+
+  if (!userEmail || recurlyEmail === userEmail) return null
+
+  return (
+    <>
+      <form onSubmit={handleSubmit}>
+        <FormGroup>
+          {isSuccess ? (
+            <Alert bsStyle="success">{t('recurly_email_updated')}</Alert>
+          ) : (
+            <>
+              <p>
+                <Trans
+                  i18nKey="recurly_email_update_needed"
+                  components={[<em />, <em />]} // eslint-disable-line react/jsx-key
+                  values={{ recurlyEmail, userEmail }}
+                />
+              </p>
+              <div>
+                <button
+                  className="btn btn-primary"
+                  type="submit"
+                  disabled={isLoading}
+                >
+                  {isLoading ? <>{t('updating')}…</> : t('update')}
+                </button>
+              </div>
+            </>
+          )}
+        </FormGroup>
+      </form>
+      <hr />
+    </>
+  )
+}
+
+export default PersonalSubscriptionRecurlySyncEmail

+ 2 - 0
services/web/frontend/js/features/subscription/components/dashboard/personal-subscription.tsx

@@ -4,6 +4,7 @@ import { ActiveSubscription } from './states/active/active'
 import { CanceledSubscription } from './states/canceled'
 import { ExpiredSubscription } from './states/expired'
 import { useSubscriptionDashboardContext } from '../../context/subscription-dashboard-context'
+import PersonalSubscriptionRecurlySyncEmail from './personal-subscription-recurly-sync-email'
 
 function PastDueSubscriptionAlert({
   subscription,
@@ -79,6 +80,7 @@ function PersonalSubscription() {
         </div>
       )}
       <hr />
+      <PersonalSubscriptionRecurlySyncEmail />
     </>
   )
 }

+ 1 - 0
services/web/locales/en.json

@@ -1607,6 +1607,7 @@
   "update_account_info": "Update Account Info",
   "update_dropbox_settings": "Update Dropbox Settings",
   "update_your_billing_details": "Update Your Billing Details",
+  "updating": "Updating",
   "updating_site": "Updating Site",
   "upgrade": "Upgrade",
   "upgrade_cc_btn": "Upgrade now, pay after 7 days",

+ 48 - 1
services/web/test/frontend/features/subscription/components/dashboard/personal-subscription.test.tsx

@@ -1,5 +1,10 @@
 import { expect } from 'chai'
-import { screen } from '@testing-library/react'
+import {
+  screen,
+  fireEvent,
+  waitForElementToBeRemoved,
+  within,
+} from '@testing-library/react'
 import PersonalSubscription from '../../../../../../frontend/js/features/subscription/components/dashboard/personal-subscription'
 import {
   annualActiveSubscription,
@@ -11,6 +16,7 @@ import {
   cleanUpContext,
   renderWithSubscriptionDashContext,
 } from '../../helpers/render-with-subscription-dash-context'
+import fetchMock from 'fetch-mock'
 
 describe('<PersonalSubscription />', function () {
   afterEach(function () {
@@ -145,4 +151,45 @@ describe('<PersonalSubscription />', function () {
       screen.getByText('Change plan')
     })
   })
+
+  it('shows different recurly email address section', async function () {
+    fetchMock.post('/user/subscription/account/email', 200)
+    const usersEmail = 'foo@example.com'
+    renderWithSubscriptionDashContext(<PersonalSubscription />, {
+      metaTags: [
+        { name: 'ol-subscription', value: annualActiveSubscription },
+        { name: 'ol-usersEmail', value: usersEmail },
+      ],
+    })
+
+    const billingText = screen.getByText(
+      /your billing email address is currently/i
+    ).textContent
+    expect(billingText).to.contain(
+      `Your billing email address is currently ${annualActiveSubscription.recurly.account.email}.` +
+        ` If needed you can update your billing address to ${usersEmail}`
+    )
+
+    const submitBtn = screen.getByRole<HTMLButtonElement>('button', {
+      name: /update/i,
+    })
+    expect(submitBtn.disabled).to.be.false
+    fireEvent.click(submitBtn)
+    expect(submitBtn.disabled).to.be.true
+    expect(
+      screen.getByRole<HTMLButtonElement>('button', { name: /updating/i })
+        .disabled
+    ).to.be.true
+
+    await waitForElementToBeRemoved(() =>
+      screen.getByText(/your billing email address is currently/i)
+    )
+
+    within(screen.getByRole('alert')).getByText(
+      /your billing email address was successfully updated/i
+    )
+
+    expect(screen.queryByRole('button', { name: /update/i })).to.be.null
+    expect(screen.queryByRole('button', { name: /updating/i })).to.be.null
+  })
 })

+ 10 - 0
services/web/test/frontend/features/subscription/fixtures/subscriptions.tsx

@@ -3,6 +3,7 @@ import {
   GroupSubscription,
   RecurlySubscription,
 } from '../../../../../types/subscription/dashboard/subscription'
+
 const dateformat = require('dateformat')
 const today = new Date()
 const oneYearFromToday = new Date().setFullYear(today.getFullYear() + 1)
@@ -45,6 +46,7 @@ export const annualActiveSubscription: RecurlySubscription = {
     trial_ends_at: null,
     activeCoupons: [],
     account: {
+      email: 'fake@example.com',
       has_canceled_subscription: { _: 'false', $: { type: 'boolean' } },
       has_past_due_invoice: { _: 'false', $: { type: 'boolean' } },
     },
@@ -84,6 +86,7 @@ export const annualActiveSubscriptionEuro: RecurlySubscription = {
     trial_ends_at: null,
     activeCoupons: [],
     account: {
+      email: 'fake@example.com',
       has_canceled_subscription: { _: 'false', $: { type: 'boolean' } },
       has_past_due_invoice: { _: 'false', $: { type: 'boolean' } },
     },
@@ -122,6 +125,7 @@ export const annualActiveSubscriptionPro: RecurlySubscription = {
     trial_ends_at: null,
     activeCoupons: [],
     account: {
+      email: 'fake@example.com',
       has_canceled_subscription: { _: 'false', $: { type: 'boolean' } },
       has_past_due_invoice: { _: 'false', $: { type: 'boolean' } },
     },
@@ -161,6 +165,7 @@ export const pastDueExpiredSubscription: RecurlySubscription = {
     trial_ends_at: null,
     activeCoupons: [],
     account: {
+      email: 'fake@example.com',
       has_canceled_subscription: { _: 'false', $: { type: 'boolean' } },
       has_past_due_invoice: { _: 'true', $: { type: 'boolean' } },
     },
@@ -200,6 +205,7 @@ export const canceledSubscription: RecurlySubscription = {
     trial_ends_at: null,
     activeCoupons: [],
     account: {
+      email: 'fake@example.com',
       has_canceled_subscription: { _: 'true', $: { type: 'boolean' } },
       has_past_due_invoice: { _: 'false', $: { type: 'boolean' } },
     },
@@ -239,6 +245,7 @@ export const pendingSubscriptionChange: RecurlySubscription = {
     trial_ends_at: null,
     activeCoupons: [],
     account: {
+      email: 'fake@example.com',
       has_canceled_subscription: { _: 'false', $: { type: 'boolean' } },
       has_past_due_invoice: { _: 'false', $: { type: 'boolean' } },
     },
@@ -289,6 +296,7 @@ export const groupActiveSubscription: GroupSubscription = {
     trial_ends_at: null,
     activeCoupons: [],
     account: {
+      email: 'fake@example.com',
       has_canceled_subscription: { _: 'false', $: { type: 'boolean' } },
       has_past_due_invoice: { _: 'false', $: { type: 'boolean' } },
     },
@@ -333,6 +341,7 @@ export const groupActiveSubscriptionWithPendingLicenseChange: GroupSubscription
       trial_ends_at: null,
       activeCoupons: [],
       account: {
+        email: 'fake@example.com',
         has_canceled_subscription: {
           _: 'false',
           $: {
@@ -395,6 +404,7 @@ export const trialSubscription: RecurlySubscription = {
     trial_ends_at: new Date(sevenDaysFromToday).toString(),
     activeCoupons: [],
     account: {
+      email: 'fake@example.com',
       has_canceled_subscription: {
         _: 'false',
         $: {

+ 2 - 0
services/web/test/frontend/features/subscription/helpers/render-with-subscription-dash-context.tsx

@@ -2,6 +2,7 @@ import { render } from '@testing-library/react'
 import _ from 'lodash'
 import { SubscriptionDashboardProvider } from '../../../../../frontend/js/features/subscription/context/subscription-dashboard-context'
 import { groupPriceByUsageTypeAndSize, plans } from '../fixtures/plans'
+import fetchMock from 'fetch-mock'
 
 export function renderWithSubscriptionDashContext(
   component: React.ReactElement,
@@ -89,4 +90,5 @@ export function cleanUpContext() {
   // @ts-ignore
   delete global.recurly
   window.metaAttributesCache = new Map()
+  fetchMock.reset()
 }

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

@@ -19,6 +19,7 @@ type Recurly = {
   trial_ends_at: Nullable<string>
   activeCoupons: any[] // TODO: confirm type in array
   account: {
+    email: string
     // data via Recurly API
     has_canceled_subscription: {
       _: 'false' | 'true'