From ba13ccdb11781176bbd8db7a5e7f92ed6ff7d7d7 Mon Sep 17 00:00:00 2001 From: Jessica Lawshe <5312836+lawshe@users.noreply.github.com> Date: Mon, 11 May 2026 10:04:15 -0500 Subject: [PATCH] Merge pull request #33202 from overleaf/jel-domain-captured-by-group-settings-page [Domain capture] Check `domainCapturedByGroup` for existing emails on user settings GitOrigin-RevId: 5ac86b89969b186cce0cac410c2957e5aa1b9703 --- .../src/Features/User/UserPagesController.mjs | 6 ++- .../settings/components/emails/row.tsx | 10 ++++- .../web/frontend/js/pages/user/settings.tsx | 7 +++- .../components/emails/emails-row.test.tsx | 41 +++++++++++++++++-- .../emails/emails-section-actions.test.tsx | 30 +++++++++----- 5 files changed, 78 insertions(+), 16 deletions(-) diff --git a/services/web/app/src/Features/User/UserPagesController.mjs b/services/web/app/src/Features/User/UserPagesController.mjs index ef7a5fd7e5..edf00d1960 100644 --- a/services/web/app/src/Features/User/UserPagesController.mjs +++ b/services/web/app/src/Features/User/UserPagesController.mjs @@ -117,7 +117,11 @@ async function settingsPage(req, res) { } await SplitTestHandler.promises.getAssignment(req, res, 'email-notifications') - + await SplitTestHandler.promises.getAssignment( + req, + res, + 'domain-captured-by-group' + ) res.render('user/settings', { title: 'account_settings', user: { diff --git a/services/web/frontend/js/features/settings/components/emails/row.tsx b/services/web/frontend/js/features/settings/components/emails/row.tsx index 0c34c99989..2ba0fd5a58 100644 --- a/services/web/frontend/js/features/settings/components/emails/row.tsx +++ b/services/web/frontend/js/features/settings/components/emails/row.tsx @@ -8,6 +8,7 @@ import Actions from './actions' import { institutionAlreadyLinked } from '../../utils/selectors' import { useUserEmailsContext } from '../../context/user-email-context' import getMeta from '../../../../utils/meta' +import { useFeatureFlag } from '@/shared/context/split-test-context' import { ssoAvailableForInstitution } from '../../utils/sso' import ReconfirmationInfo from './reconfirmation-info' import { useLocation } from '../../../../shared/hooks/use-location' @@ -71,6 +72,10 @@ function SSOAffiliationInfo({ userEmailData }: SSOAffiliationInfoProps) { const [linkAccountsButtonDisabled, setLinkAccountsButtonDisabled] = useState(false) + const domainCapturedByGroupRolloutFlagEnabled = useFeatureFlag( + 'domain-captured-by-group' + ) + function handleLinkAccountsButtonClick() { setLinkAccountsButtonDisabled(true) location.assign( @@ -140,7 +145,10 @@ function SSOAffiliationInfo({ userEmailData }: SSOAffiliationInfoProps) { } const domainAlsoForGroupWithDomainCapture = - userEmailData?.affiliation?.group?.domainCaptureEnabled + domainCapturedByGroupRolloutFlagEnabled + ? userEmailData?.affiliation?.domainCapturedByGroup && + userEmailData?.affiliation?.group?.domainCaptureEnabled + : userEmailData?.affiliation?.group?.domainCaptureEnabled if (domainAlsoForGroupWithDomainCapture) { // user is not linked via Commons and should link via groups diff --git a/services/web/frontend/js/pages/user/settings.tsx b/services/web/frontend/js/pages/user/settings.tsx index a425877180..b84cdb1416 100644 --- a/services/web/frontend/js/pages/user/settings.tsx +++ b/services/web/frontend/js/pages/user/settings.tsx @@ -4,10 +4,15 @@ import '@/utils/webpack-public-path' import '@/infrastructure/error-reporter' import '@/i18n' import SettingsPageRoot from '@/features/settings/components/root' +import { SplitTestProvider } from '@/shared/context/split-test-context' // For react-google-recaptcha window.recaptchaOptions = { enterprise: true, useRecaptchaNet: true, } -renderInReactLayout('settings-page-root', () => ) +renderInReactLayout('settings-page-root', () => ( + + + +)) diff --git a/services/web/test/frontend/features/settings/components/emails/emails-row.test.tsx b/services/web/test/frontend/features/settings/components/emails/emails-row.test.tsx index 9fcd48bebf..4f8fe2b5ce 100644 --- a/services/web/test/frontend/features/settings/components/emails/emails-row.test.tsx +++ b/services/web/test/frontend/features/settings/components/emails/emails-row.test.tsx @@ -11,12 +11,15 @@ import { UserEmailData } from '../../../../../../types/user-email' import { UserEmailsProvider } from '../../../../../../frontend/js/features/settings/context/user-email-context' import { Affiliation } from '../../../../../../types/affiliation' import getMeta from '@/utils/meta' +import { SplitTestProvider } from '@/shared/context/split-test-context' function renderEmailsRow(data: UserEmailData) { return render( - - - + + + + + ) } @@ -33,6 +36,7 @@ describe('', function () { samlInitPath: '/saml', hasSamlBeta: true, }) + window.metaAttributesCache.set('ol-splitTestVariants', {}) fetchMock.get('/user/emails?ensureAffiliation=true', []) }) @@ -165,6 +169,37 @@ describe('', function () { expect(screen.queryByRole('button', { name: 'Link accounts' })).to.be .null }) + + it('uses `domainCapturedByGroup` when the feature flag is enabled', function () { + affiliatedEmailWithDomainCaptureAndCommons.affiliation.group = { + _id: 'grou123', + domainCaptureEnabled: true, + managedUsersEnabled: true, + } + affiliatedEmailWithDomainCaptureAndCommons.affiliation.domainCapturedByGroup = true + window.metaAttributesCache.set('ol-splitTestVariants', { + 'domain-captured-by-group': 'enabled', + }) + + renderEmailsRow(affiliatedEmailWithDomainCaptureAndCommons) + + expect(screen.queryByRole('button', { name: 'Link accounts' })).to.be + .null + }) + + it('ignores group domain capture when the feature flag is enabled and the domain is not captured', function () { + affiliatedEmailWithDomainCaptureAndCommons.affiliation.domainCapturedByGroup = false + window.metaAttributesCache.set('ol-splitTestVariants', { + 'domain-captured-by-group': 'enabled', + }) + + renderEmailsRow(affiliatedEmailWithDomainCaptureAndCommons) + + getByTextContent( + 'You can now link your Overleaf account to your Overleaf institutional account.' + ) + screen.getByRole('button', { name: 'Link accounts' }) + }) }) }) }) diff --git a/services/web/test/frontend/features/settings/components/emails/emails-section-actions.test.tsx b/services/web/test/frontend/features/settings/components/emails/emails-section-actions.test.tsx index fca5714fe9..0e443bad41 100644 --- a/services/web/test/frontend/features/settings/components/emails/emails-section-actions.test.tsx +++ b/services/web/test/frontend/features/settings/components/emails/emails-section-actions.test.tsx @@ -13,6 +13,7 @@ import EmailsSection from '../../../../../../frontend/js/features/settings/compo import { Institution } from '../../../../../../types/institution' import { Affiliation } from '../../../../../../types/affiliation' import getMeta from '@/utils/meta' +import { SplitTestProvider } from '@/shared/context/split-test-context' const userEmailData: UserEmailData = { confirmedAt: '2022-03-10T10:59:44.139Z', @@ -32,6 +33,14 @@ const userEmailData2: UserEmailData & { affiliation: Affiliation } = { default: false, } +function renderEmailsSection() { + return render(, { + wrapper: ({ children }) => ( + {children} + ), + }) +} + describe('email actions - make primary', function () { beforeEach(function () { Object.assign(getMeta('ol-ExposedSettings'), { @@ -49,7 +58,7 @@ describe('email actions - make primary', function () { const userEmailDataCopy = { ...userEmailData2 } const { confirmedAt: _, ...userEmailData } = userEmailDataCopy fetchMock.get('/user/emails?ensureAffiliation=true', [userEmailData]) - render() + renderEmailsSection() const button = (await screen.findByRole('button', { name: /make primary/i, @@ -66,7 +75,7 @@ describe('email actions - make primary', function () { }, } fetchMock.get('/user/emails?ensureAffiliation=true', [userEmailDataCopy]) - render() + renderEmailsSection() const button = (await screen.findByRole('button', { name: /make primary/i, @@ -86,7 +95,7 @@ describe('email actions - make primary', function () { } fetchMock.get('/user/emails?ensureAffiliation=true', [userEmailDataCopy]) - render() + renderEmailsSection() const button = (await screen.findByRole('button', { name: /make primary/i, @@ -103,7 +112,7 @@ describe('email actions - make primary', function () { const userEmailDataCopy = { ...userEmailData2 } fetchMock.get('/user/emails?ensureAffiliation=true', [userEmailDataCopy]) - render() + renderEmailsSection() const button = (await screen.findByRole('button', { name: /make primary/i, @@ -144,7 +153,7 @@ describe('email actions - make primary', function () { userEmailDataCopy1, userEmailDataCopy2, ]) - render() + renderEmailsSection() const buttons = (await screen.findAllByRole('button', { name: /make primary/i, @@ -177,7 +186,7 @@ describe('email actions - make primary', function () { it('shows confirmation modal and closes it', async function () { fetchMock.get('/user/emails?ensureAffiliation=true', [userEmailData]) - render() + renderEmailsSection() const button = await screen.findByRole('button', { name: /make primary/i, @@ -205,7 +214,7 @@ describe('email actions - make primary', function () { fetchMock .get('/user/emails?ensureAffiliation=true', [userEmailData]) .post('/user/emails/default?delete-unconfirmed-primary', 200) - render() + renderEmailsSection() await confirmPrimaryEmail() @@ -221,7 +230,7 @@ describe('email actions - make primary', function () { fetchMock .get('/user/emails?ensureAffiliation=true', [userEmailData]) .post('/user/emails/default?delete-unconfirmed-primary', 503) - render() + renderEmailsSection() await confirmPrimaryEmail() @@ -241,13 +250,14 @@ describe('email actions - delete', function () { afterEach(function () { fetchMock.removeRoutes().clearHistory() + window.metaAttributesCache.set('ol-splitTestVariants', {}) }) it('shows loader when deleting and removes the row', async function () { fetchMock .get('/user/emails?ensureAffiliation=true', [userEmailData]) .post('/user/emails/delete', 200) - render() + renderEmailsSection() const button = await screen.findByRole('button', { name: /remove/i }) fireEvent.click(button) @@ -261,7 +271,7 @@ describe('email actions - delete', function () { fetchMock .get('/user/emails?ensureAffiliation=true', [userEmailData]) .post('/user/emails/delete', 503) - render() + renderEmailsSection() const button = await screen.findByRole('button', { name: /remove/i }) fireEvent.click(button)