diff --git a/services/web/frontend/js/features/settings/components/linking-section.tsx b/services/web/frontend/js/features/settings/components/linking-section.tsx
index 3c69351da6..7807e0d726 100644
--- a/services/web/frontend/js/features/settings/components/linking-section.tsx
+++ b/services/web/frontend/js/features/settings/components/linking-section.tsx
@@ -1,17 +1,25 @@
+import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
import { useSSOContext, SSOSubscription } from '../context/sso-context'
import { SSOLinkingWidget } from './linking/sso-widget'
-
-const integrationLinkingWidgets = importOverleafModules(
- 'integrationLinkingWidgets'
-)
-const referenceLinkingWidgets = importOverleafModules('referenceLinkingWidgets')
+import getMeta from '../../../utils/meta'
function LinkingSection() {
const { t } = useTranslation()
const { subscriptions } = useSSOContext()
+ const [integrationLinkingWidgets] = useState(
+ () =>
+ getMeta('integrationLinkingWidgets') ||
+ importOverleafModules('integrationLinkingWidgets')
+ )
+ const [referenceLinkingWidgets] = useState(
+ () =>
+ getMeta('referenceLinkingWidgets') ||
+ importOverleafModules('referenceLinkingWidgets')
+ )
+
const hasIntegrationLinkingSection = integrationLinkingWidgets.length
const hasReferencesLinkingSection = referenceLinkingWidgets.length
const hasSSOLinkingSection = Object.keys(subscriptions).length > 0
diff --git a/services/web/frontend/js/features/settings/components/root.tsx b/services/web/frontend/js/features/settings/components/root.tsx
index 5481033963..4731ae2dfa 100644
--- a/services/web/frontend/js/features/settings/components/root.tsx
+++ b/services/web/frontend/js/features/settings/components/root.tsx
@@ -64,12 +64,16 @@ function SettingsPageContent() {
- {isOverleaf ? : null}
-
-
-
{isOverleaf ? (
<>
+
+
+ >
+ ) : null}
+
+ {isOverleaf ? (
+ <>
+
diff --git a/services/web/frontend/js/features/settings/context/sso-context.tsx b/services/web/frontend/js/features/settings/context/sso-context.tsx
index e9f3c1712a..0cc0583c30 100644
--- a/services/web/frontend/js/features/settings/context/sso-context.tsx
+++ b/services/web/frontend/js/features/settings/context/sso-context.tsx
@@ -35,7 +35,7 @@ type SSOProviderProps = {
export function SSOProvider({ children }: SSOProviderProps) {
const isMountedRef = useIsMounted()
- const oauthProviders = getMeta('ol-oauthProviders') as OAuthProviders
+ const oauthProviders = getMeta('ol-oauthProviders', {}) as OAuthProviders
const thirdPartyIds = getMeta('ol-thirdPartyIds') as ThirdPartyIds
const [subscriptions, setSubscriptions] = useState<
diff --git a/services/web/frontend/stories/settings/helpers/account-info.js b/services/web/frontend/stories/settings/helpers/account-info.js
index cebd81e7bc..e3a893f948 100644
--- a/services/web/frontend/stories/settings/helpers/account-info.js
+++ b/services/web/frontend/stories/settings/helpers/account-info.js
@@ -9,11 +9,13 @@ export function defaultSetupMocks(fetchMock) {
export function setDefaultMeta() {
window.metaAttributesCache = window.metaAttributesCache || new Map()
window.metaAttributesCache.set('ol-user', {
+ ...window.metaAttributesCache.get('ol-user'),
email: 'sherlock@holmes.co.uk',
first_name: 'Sherlock',
last_name: 'Holmes',
})
window.metaAttributesCache.set('ol-ExposedSettings', {
+ ...window.metaAttributesCache.get('ol-ExposedSettings'),
hasAffiliationsFeature: false,
})
window.metaAttributesCache.set('ol-isExternalAuthenticationSystemUsed', false)
diff --git a/services/web/frontend/stories/settings/helpers/emails.js b/services/web/frontend/stories/settings/helpers/emails.js
index 1a74d17f55..02871c504e 100644
--- a/services/web/frontend/stories/settings/helpers/emails.js
+++ b/services/web/frontend/stories/settings/helpers/emails.js
@@ -72,6 +72,7 @@ export function errorsMocks(fetchMock) {
export function setDefaultMeta() {
window.metaAttributesCache = window.metaAttributesCache || new Map()
window.metaAttributesCache.set('ol-ExposedSettings', {
+ ...window.metaAttributesCache.get('ol-ExposedSettings'),
hasAffiliationsFeature: true,
hasSamlFeature: true,
samlInitPath: 'saml/init',
diff --git a/services/web/frontend/stories/settings/helpers/leave.js b/services/web/frontend/stories/settings/helpers/leave.js
index 7e5003afc1..8b74f03c1b 100644
--- a/services/web/frontend/stories/settings/helpers/leave.js
+++ b/services/web/frontend/stories/settings/helpers/leave.js
@@ -9,6 +9,9 @@ export function defaultSetupMocks(fetchMock) {
export function setDefaultMeta() {
window.metaAttributesCache = window.metaAttributesCache || new Map()
window.metaAttributesCache.set('ol-usersEmail', 'user@primary.com')
- window.metaAttributesCache.set('ol-ExposedSettings', { isOverleaf: true })
+ window.metaAttributesCache.set('ol-ExposedSettings', {
+ ...window.metaAttributesCache.get('ol-ExposedSettings'),
+ isOverleaf: true,
+ })
window.metaAttributesCache.set('ol-hasPassword', true)
}
diff --git a/services/web/frontend/stories/settings/helpers/linking.js b/services/web/frontend/stories/settings/helpers/linking.js
index 647fcb0427..410f43a956 100644
--- a/services/web/frontend/stories/settings/helpers/linking.js
+++ b/services/web/frontend/stories/settings/helpers/linking.js
@@ -12,6 +12,7 @@ export function defaultSetupMocks(fetchMock) {
export function setDefaultMeta() {
window.metaAttributesCache.set('ol-user', {
+ ...window.metaAttributesCache.get('ol-user'),
features: { github: true, dropbox: true, mendeley: false, zotero: false },
refProviders: {
mendeley: true,
@@ -55,4 +56,6 @@ export function setDefaultMeta() {
linkPath: '/auth/twitter',
},
})
+ window.metaAttributesCache.delete('integrationLinkingWidgets')
+ window.metaAttributesCache.delete('referenceLinkingWidgets')
}
diff --git a/services/web/frontend/stories/settings/helpers/password.js b/services/web/frontend/stories/settings/helpers/password.js
index d7ede1d1bb..ebc58cc38e 100644
--- a/services/web/frontend/stories/settings/helpers/password.js
+++ b/services/web/frontend/stories/settings/helpers/password.js
@@ -22,6 +22,7 @@ export function defaultSetupMocks(fetchMock) {
export function setDefaultMeta() {
window.metaAttributesCache = window.metaAttributesCache || new Map()
window.metaAttributesCache.set('ol-ExposedSettings', {
+ ...window.metaAttributesCache.get('ol-ExposedSettings'),
isOverleaf: true,
})
window.metaAttributesCache.set('ol-isExternalAuthenticationSystemUsed', false)
diff --git a/services/web/frontend/stories/settings/page.stories.js b/services/web/frontend/stories/settings/page.stories.js
index a37073405e..26e456d4d7 100644
--- a/services/web/frontend/stories/settings/page.stories.js
+++ b/services/web/frontend/stories/settings/page.stories.js
@@ -22,7 +22,7 @@ import {
} from './helpers/linking'
import { UserProvider } from '../../js/shared/context/user-context'
-export const Root = args => {
+export const Overleaf = args => {
setDefaultLeaveMeta()
setDefaultAccountInfoMeta()
setDefaultPasswordMeta()
@@ -43,6 +43,30 @@ export const Root = args => {
)
}
+export const ServerPro = args => {
+ setDefaultAccountInfoMeta()
+ setDefaultPasswordMeta()
+ useFetchMock(fetchMock => {
+ defaultSetupAccountInfoMocks(fetchMock)
+ defaultSetupPasswordMocks(fetchMock)
+ })
+
+ window.metaAttributesCache.set('ol-ExposedSettings', {
+ ...window.metaAttributesCache.get('ol-ExposedSettings'),
+ hasAffiliationsFeature: false,
+ isOverleaf: false,
+ })
+ window.metaAttributesCache.set('integrationLinkingWidgets', [])
+ window.metaAttributesCache.set('referenceLinkingWidgets', [])
+ window.metaAttributesCache.delete('ol-oauthProviders')
+
+ return (
+
+
+
+ )
+}
+
export default {
title: 'Account Settings / Full Page',
component: SettingsPageRoot,
diff --git a/services/web/test/frontend/features/settings/components/linking-section.test.tsx b/services/web/test/frontend/features/settings/components/linking-section.test.tsx
new file mode 100644
index 0000000000..1492a2d75e
--- /dev/null
+++ b/services/web/test/frontend/features/settings/components/linking-section.test.tsx
@@ -0,0 +1,98 @@
+import { expect } from 'chai'
+import { screen, render } from '@testing-library/react'
+import fetchMock from 'fetch-mock'
+import LinkingSection from '../../../../../frontend/js/features/settings/components/linking-section'
+import { UserProvider } from '../../../../../frontend/js/shared/context/user-context'
+import { SSOProvider } from '../../../../../frontend/js/features/settings/context/sso-context'
+
+function renderSectionWithProviders() {
+ render(, {
+ wrapper: ({ children }) => (
+
+ {children}
+
+ ),
+ })
+}
+
+const mockOauthProviders = {
+ google: {
+ descriptionKey: 'login_with_service',
+ descriptionOptions: { service: 'Google' },
+ name: 'Google',
+ linkPath: '/auth/google',
+ },
+ orcid: {
+ descriptionKey: 'oauth_orcid_description',
+ descriptionOptions: {
+ link: '/blog/434',
+ appName: 'Overleaf',
+ },
+ name: 'Orcid',
+ linkPath: '/auth/orcid',
+ },
+ twitter: {
+ hideWhenNotLinked: true,
+ name: 'Twitter',
+ linkPath: '/auth/twitter',
+ },
+}
+
+describe('', function () {
+ beforeEach(function () {
+ window.metaAttributesCache = window.metaAttributesCache || new Map()
+ window.metaAttributesCache.set('ol-user', {})
+
+ // suppress integrations and references widgets as they cannot be tested in
+ // all environments
+ window.metaAttributesCache.set('integrationLinkingWidgets', [])
+ window.metaAttributesCache.set('referenceLinkingWidgets', [])
+
+ window.metaAttributesCache.set('ol-thirdPartyIds', {
+ google: 'google-id',
+ })
+
+ window.metaAttributesCache.set('ol-oauthProviders', mockOauthProviders)
+ })
+
+ afterEach(function () {
+ window.metaAttributesCache = new Map()
+ fetchMock.reset()
+ })
+
+ it('shows header', async function () {
+ renderSectionWithProviders()
+
+ screen.getByText('Integrations')
+ screen.getByText(
+ 'You can link your Overleaf account with other services to enable the features described below'
+ )
+ })
+
+ it('lists SSO providers', async function () {
+ renderSectionWithProviders()
+ screen.getByText('linked accounts')
+
+ screen.getByText('Google')
+ screen.getByText('Log in with Google')
+ screen.getByRole('button', { name: 'Unlink' })
+
+ screen.getByText('Orcid')
+ screen.getByText(
+ /Securely establish your identity by linking your ORCID iD/
+ )
+ const helpLink = screen.getByRole('link', { name: 'Learn more' })
+ expect(helpLink.getAttribute('href')).to.equal('/blog/434')
+ const linkButton = screen.getByRole('link', { name: 'Link' })
+ expect(linkButton.getAttribute('href')).to.equal('/auth/orcid?intent=link')
+
+ expect(screen.queryByText('Twitter')).to.not.exist
+ })
+
+ it('does not show providers section when empty', async function () {
+ window.metaAttributesCache.delete('ol-oauthProviders')
+ renderSectionWithProviders()
+
+ expect(screen.queryByText('linked accounts')).to.not.exist
+ })
+})