diff --git a/services/web/frontend/js/features/settings/components/emails/email.tsx b/services/web/frontend/js/features/settings/components/emails/email.tsx
index c0cf98e8bc..0a56d317ed 100644
--- a/services/web/frontend/js/features/settings/components/emails/email.tsx
+++ b/services/web/frontend/js/features/settings/components/emails/email.tsx
@@ -1,9 +1,9 @@
import { useTranslation } from 'react-i18next'
import { UserEmailData } from '../../../../../../types/user-email'
-import { ssoAvailableForInstitution } from '../../utils/sso'
import OLBadge from '@/shared/components/ol/ol-badge'
import ResendConfirmationCodeModal from '@/features/settings/components/emails/resend-confirmation-code-modal'
import { useUserEmailsContext } from '@/features/settings/context/user-email-context'
+import { emailMustBeConfirmedViaSAML } from '../../utils/email-confirmation'
type EmailProps = {
userEmailData: UserEmailData
@@ -16,8 +16,8 @@ function Email({ userEmailData }: EmailProps) {
setLoading: setUserEmailsContextLoading,
getEmails,
} = useUserEmailsContext()
- const ssoAvailable = ssoAvailableForInstitution(
- userEmailData.affiliation?.institution || null
+ const mustConfirmViaSAML = emailMustBeConfirmedViaSAML(
+ userEmailData.affiliation || null
)
const isPrimary = userEmailData.default
@@ -37,7 +37,7 @@ function Email({ userEmailData }: EmailProps) {
{t('unconfirmed')}.
- {!ssoAvailable && (
+ {!mustConfirmViaSAML && (
{
+ const { hasSamlFeature } = getMeta('ol-ExposedSettings')
+ if (
+ !hasSamlFeature ||
+ !affiliation ||
+ !affiliation.institution ||
+ !affiliation.institution.confirmed
+ ) {
+ return false
+ }
+ // both groups and Commons have affiliation.institution, but only Commons has SAML
+ // information in affiliation.institution
+ // ssoAvailableForInstitution will only check affiliation.institution for Commons
+ // and there are different rules for whether SAML is enabled or not for groups with
+ // domain capture or Commons
+
+ const samlViaCommons = ssoAvailableForInstitution(affiliation.institution)
+
+ const samlViaGroup =
+ affiliation.domainCapturedByGroup &&
+ affiliation.group?.domainCaptureEnabled &&
+ affiliation.group?.managedUsersEnabled
+
+ return samlViaCommons || samlViaGroup || false
+}
diff --git a/services/web/test/frontend/features/settings/components/emails/emails-section.test.tsx b/services/web/test/frontend/features/settings/components/emails/emails-section.test.tsx
index 5505d66ac3..bfe8fe0e1d 100644
--- a/services/web/test/frontend/features/settings/components/emails/emails-section.test.tsx
+++ b/services/web/test/frontend/features/settings/components/emails/emails-section.test.tsx
@@ -9,6 +9,7 @@ import {
import EmailsSection from '../../../../../../frontend/js/features/settings/components/emails-section'
import { expect } from 'chai'
import fetchMock from 'fetch-mock'
+import { cloneDeep } from 'lodash'
import {
confirmedUserData,
fakeUsersData,
@@ -111,6 +112,46 @@ describe('', function () {
await screen.findByRole('button', { name: 'Send confirmation code' })
})
+ describe('SAML emails', function () {
+ beforeEach(function () {
+ Object.assign(getMeta('ol-ExposedSettings'), {
+ hasSamlFeature: true,
+ })
+ fetchMock.removeRoutes().clearHistory()
+ })
+
+ it('hides resend link when an unconfirmed affiliated email must be confirmed via Commons SAML', async function () {
+ const commonsSamlEmail = cloneDeep(professionalUserData)
+ delete commonsSamlEmail.confirmedAt
+ commonsSamlEmail.affiliation.institution.ssoEnabled = true
+
+ fetchMock.get('/user/emails?ensureAffiliation=true', [commonsSamlEmail])
+ renderEmailsSection()
+
+ await screen.findByText(/unconfirmed/i)
+ expect(screen.queryByRole('button', { name: 'Send confirmation code' }))
+ .to.be.null
+ })
+
+ it('hides resend link when an unconfirmed affiliated email must be confirmed via managed group SAML', async function () {
+ const managedGroupEmail = cloneDeep(professionalUserData)
+ delete managedGroupEmail.confirmedAt
+ managedGroupEmail.affiliation.domainCapturedByGroup = true
+ managedGroupEmail.affiliation.group = {
+ _id: 'group123',
+ domainCaptureEnabled: true,
+ managedUsersEnabled: true,
+ }
+
+ fetchMock.get('/user/emails?ensureAffiliation=true', [managedGroupEmail])
+ renderEmailsSection()
+
+ await screen.findByText(/unconfirmed/i)
+ expect(screen.queryByRole('button', { name: 'Send confirmation code' }))
+ .to.be.null
+ })
+ })
+
it('renders commons label', async function () {
fetchMock.get('/user/emails?ensureAffiliation=true', [professionalUserData])
renderEmailsSection()
diff --git a/services/web/test/frontend/features/settings/utils/email-confirmation.test.ts b/services/web/test/frontend/features/settings/utils/email-confirmation.test.ts
new file mode 100644
index 0000000000..c61cda0cec
--- /dev/null
+++ b/services/web/test/frontend/features/settings/utils/email-confirmation.test.ts
@@ -0,0 +1,91 @@
+import { expect } from 'chai'
+import { cloneDeep } from 'lodash'
+import { professionalUserData } from '../fixtures/test-user-email-data'
+import { Affiliation } from '../../../../../types/affiliation'
+import { emailMustBeConfirmedViaSAML } from '../../../../../frontend/js/features/settings/utils/email-confirmation'
+
+describe('emailMustBeConfirmedViaSAML', function () {
+ beforeEach(function () {
+ window.metaAttributesCache.set('ol-ExposedSettings', {
+ hasSamlFeature: true,
+ hasSamlBeta: false,
+ })
+ })
+
+ it('returns false when hasSamlFeature is false', function () {
+ window.metaAttributesCache.set('ol-ExposedSettings', {
+ hasSamlFeature: false,
+ hasSamlBeta: false,
+ })
+
+ const affiliation: Affiliation = cloneDeep(professionalUserData.affiliation)
+ affiliation.domainCapturedByGroup = true
+ affiliation.group = {
+ _id: 'group123',
+ domainCaptureEnabled: true,
+ managedUsersEnabled: true,
+ }
+
+ expect(emailMustBeConfirmedViaSAML(affiliation)).to.be.false
+ })
+
+ it('returns false when affiliation is null', function () {
+ expect(emailMustBeConfirmedViaSAML(null)).to.be.false
+ })
+
+ it('returns false when institution is not confirmed', function () {
+ const affiliation: Affiliation = cloneDeep(professionalUserData.affiliation)
+ affiliation.institution.confirmed = false
+ affiliation.domainCapturedByGroup = true
+ affiliation.group = {
+ _id: 'group123',
+ domainCaptureEnabled: true,
+ managedUsersEnabled: true,
+ }
+
+ expect(emailMustBeConfirmedViaSAML(affiliation)).to.be.false
+ })
+
+ it('returns true when Commons SSO is available for the institution', function () {
+ const affiliation: Affiliation = cloneDeep(professionalUserData.affiliation)
+ affiliation.institution.ssoEnabled = true
+
+ expect(emailMustBeConfirmedViaSAML(affiliation)).to.be.true
+ })
+
+ it('returns true when the email is captured by a managed domain-capture group', function () {
+ const affiliation: Affiliation = cloneDeep(professionalUserData.affiliation)
+ affiliation.domainCapturedByGroup = true
+ affiliation.group = {
+ _id: 'group123',
+ domainCaptureEnabled: true,
+ managedUsersEnabled: true,
+ }
+
+ expect(emailMustBeConfirmedViaSAML(affiliation)).to.be.true
+ })
+
+ it('returns false when the group is managed but the email domain is not captured by the group', function () {
+ const affiliation: Affiliation = cloneDeep(professionalUserData.affiliation)
+ affiliation.domainCapturedByGroup = false
+ affiliation.group = {
+ _id: 'group123',
+ domainCaptureEnabled: true,
+ managedUsersEnabled: true,
+ }
+
+ expect(emailMustBeConfirmedViaSAML(affiliation)).to.be.false
+ })
+
+ it('returns false when the email is captured but managed users are disabled', function () {
+ const affiliation: Affiliation = cloneDeep(professionalUserData.affiliation)
+ affiliation.domainCapturedByGroup = true
+ affiliation.group = {
+ _id: 'group123',
+ domainCaptureEnabled: true,
+ managedUsersEnabled: false,
+ }
+
+ expect(emailMustBeConfirmedViaSAML(affiliation)).to.be.false
+ })
+})