diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json
index 827288e568..610c4d5c51 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/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": "",
diff --git a/services/web/frontend/js/features/subscription/components/dashboard/institution-memberships.tsx b/services/web/frontend/js/features/subscription/components/dashboard/institution-memberships.tsx
index 3aaa397697..5894ffaff2 100644
--- a/services/web/frontend/js/features/subscription/components/dashboard/institution-memberships.tsx
+++ b/services/web/frontend/js/features/subscription/components/dashboard/institution-memberships.tsx
@@ -20,32 +20,32 @@ function InstitutionMemberships() {
)
}
+ if (!institutionMemberships.length) return null
+
return (
- <>
-
- {institutionMemberships.map((institution: Institution) => (
-
- ,
- // eslint-disable-next-line react/jsx-key
- ,
- // eslint-disable-next-line react/jsx-key
- ,
- ]}
- />
-
-
- ))}
- {institutionMemberships.length > 0 &&
}
-
- >
+
+ {institutionMemberships.map((institution: Institution) => (
+
+ ,
+ // eslint-disable-next-line react/jsx-key
+ ,
+ // eslint-disable-next-line react/jsx-key
+ ,
+ ]}
+ />
+
+
+ ))}
+
+
)
}
diff --git a/services/web/frontend/js/features/subscription/components/dashboard/personal-subscription-recurly-sync-email.tsx b/services/web/frontend/js/features/subscription/components/dashboard/personal-subscription-recurly-sync-email.tsx
new file mode 100644
index 0000000000..352791ea68
--- /dev/null
+++ b/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 (
+ <>
+
+
+ >
+ )
+}
+
+export default PersonalSubscriptionRecurlySyncEmail
diff --git a/services/web/frontend/js/features/subscription/components/dashboard/personal-subscription.tsx b/services/web/frontend/js/features/subscription/components/dashboard/personal-subscription.tsx
index d17ad66f15..237c56b3aa 100644
--- a/services/web/frontend/js/features/subscription/components/dashboard/personal-subscription.tsx
+++ b/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() {
)}
+
>
)
}
diff --git a/services/web/locales/en.json b/services/web/locales/en.json
index dbb34d4373..8265964e85 100644
--- a/services/web/locales/en.json
+++ b/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",
diff --git a/services/web/test/frontend/features/subscription/components/dashboard/personal-subscription.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/personal-subscription.test.tsx
index e888396c05..89ed466425 100644
--- a/services/web/test/frontend/features/subscription/components/dashboard/personal-subscription.test.tsx
+++ b/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('', function () {
afterEach(function () {
@@ -145,4 +151,45 @@ describe('', 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(, {
+ 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('button', {
+ name: /update/i,
+ })
+ expect(submitBtn.disabled).to.be.false
+ fireEvent.click(submitBtn)
+ expect(submitBtn.disabled).to.be.true
+ expect(
+ screen.getByRole('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
+ })
})
diff --git a/services/web/test/frontend/features/subscription/fixtures/subscriptions.tsx b/services/web/test/frontend/features/subscription/fixtures/subscriptions.tsx
index 2229c8a753..5926e86ccc 100644
--- a/services/web/test/frontend/features/subscription/fixtures/subscriptions.tsx
+++ b/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',
$: {
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 5dbe9a18c5..593d095577 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
@@ -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()
}
diff --git a/services/web/types/subscription/dashboard/subscription.ts b/services/web/types/subscription/dashboard/subscription.ts
index 0a2b6e3482..f10557bb4e 100644
--- a/services/web/types/subscription/dashboard/subscription.ts
+++ b/services/web/types/subscription/dashboard/subscription.ts
@@ -19,6 +19,7 @@ type Recurly = {
trial_ends_at: Nullable
activeCoupons: any[] // TODO: confirm type in array
account: {
+ email: string
// data via Recurly API
has_canceled_subscription: {
_: 'false' | 'true'