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 ( + <> +
+ + {isSuccess ? ( + {t('recurly_email_updated')} + ) : ( + <> +

+ , ]} // eslint-disable-line react/jsx-key + values={{ recurlyEmail, userEmail }} + /> +

+
+ +
+ + )} +
+ +
+ + ) +} + +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'