From 82da253ef15d4ebd51736269accda0b2ee14a5cf Mon Sep 17 00:00:00 2001 From: Andreas Kienle Date: Wed, 23 Jul 2025 14:59:50 +0200 Subject: [PATCH 1/3] Support themes --- .../Core/DarkModeSystemSwitcher.tsx | 30 ---------------- .../ThemeManager/ThemeManager.spec.ts | 21 +++++++++++ src/components/ThemeManager/ThemeManager.tsx | 25 +++++++++++++ src/components/Yaml/YamlViewer.tsx | 19 +++------- src/hooks/useIsDarkModePreferred.ts | 22 ++++++++++++ src/lib/useThemeMode.ts | 7 ---- src/main.tsx | 4 +-- .../onboarding/auth/AuthContextOnboarding.tsx | 36 ++++++------------- 8 files changed, 86 insertions(+), 78 deletions(-) delete mode 100644 src/components/Core/DarkModeSystemSwitcher.tsx create mode 100644 src/components/ThemeManager/ThemeManager.spec.ts create mode 100644 src/components/ThemeManager/ThemeManager.tsx create mode 100644 src/hooks/useIsDarkModePreferred.ts delete mode 100644 src/lib/useThemeMode.ts diff --git a/src/components/Core/DarkModeSystemSwitcher.tsx b/src/components/Core/DarkModeSystemSwitcher.tsx deleted file mode 100644 index 37c07281..00000000 --- a/src/components/Core/DarkModeSystemSwitcher.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { useEffect } from 'react'; -import { setTheme } from '@ui5/webcomponents-base/dist/config/Theme.js'; - -export function DarkModeSystemSwitcher() { - useEffect(() => { - if (!window.matchMedia) { - console.warn( - 'Dark mode system switcher is not supported in this browser', - ); - return; - } - - const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); - - const handleChange = (e: MediaQueryListEvent) => { - setTheme(e.matches ? 'sap_horizon_dark' : 'sap_horizon'); - }; - - mediaQuery.addEventListener('change', handleChange); - - const initialMode = mediaQuery.matches ? 'sap_horizon_dark' : 'sap_horizon'; - setTheme(initialMode); - - return () => { - mediaQuery.removeEventListener('change', handleChange); - }; - }, []); - - return null; // albo <> -} diff --git a/src/components/ThemeManager/ThemeManager.spec.ts b/src/components/ThemeManager/ThemeManager.spec.ts new file mode 100644 index 00000000..144b02ad --- /dev/null +++ b/src/components/ThemeManager/ThemeManager.spec.ts @@ -0,0 +1,21 @@ +import { describe, it, expect } from 'vitest'; +import { resolveTheme } from './ThemeManager.tsx'; + +describe('ThemeManager', () => { + describe('resolveTheme()', () => { + it('returns theme coming from URL when it is truthy', () => { + expect(resolveTheme('sap_fiori_3', true)).toBe('sap_fiori_3'); + expect(resolveTheme('custom_theme', false)).toBe('custom_theme'); + }); + + it('falls back to dark default when URL theme is falsy and user prefers dark mode', () => { + expect(resolveTheme(null, true)).toBe('sap_horizon_dark'); + expect(resolveTheme('', true)).toBe('sap_horizon_dark'); + }); + + it('falls back to light default when URL theme is falsy and user prefers light mode', () => { + expect(resolveTheme(null, false)).toBe('sap_horizon'); + expect(resolveTheme('', false)).toBe('sap_horizon'); + }); + }); +}); diff --git a/src/components/ThemeManager/ThemeManager.tsx b/src/components/ThemeManager/ThemeManager.tsx new file mode 100644 index 00000000..a4f864f5 --- /dev/null +++ b/src/components/ThemeManager/ThemeManager.tsx @@ -0,0 +1,25 @@ +import { useEffect } from 'react'; +import { setTheme } from '@ui5/webcomponents-base/dist/config/Theme.js'; +import { useIsDarkModePreferred } from '../../hooks/useIsDarkModePreferred.ts'; + +const DEFAULT_THEME_LIGHT = 'sap_horizon'; +const DEFAULT_THEME_DARK = 'sap_horizon_dark'; + +export function resolveTheme(themeFromUrl: string | null, isDarkModePreferred: boolean): string { + if (themeFromUrl) { + return themeFromUrl; + } + return isDarkModePreferred ? DEFAULT_THEME_DARK : DEFAULT_THEME_LIGHT; +} + +export function ThemeManager() { + const isDarkModePreferred = useIsDarkModePreferred(); + const themeFromUrl = new URL(window.location.href).searchParams.get('sap-theme'); + + useEffect(() => { + const resolvedTheme = resolveTheme(themeFromUrl, isDarkModePreferred); + void setTheme(resolvedTheme); + }, [isDarkModePreferred, themeFromUrl]); + + return null; +} diff --git a/src/components/Yaml/YamlViewer.tsx b/src/components/Yaml/YamlViewer.tsx index dd7a1db1..3828ddca 100644 --- a/src/components/Yaml/YamlViewer.tsx +++ b/src/components/Yaml/YamlViewer.tsx @@ -1,20 +1,17 @@ import { FC } from 'react'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; -import { - materialLight, - materialDark, -} from 'react-syntax-highlighter/dist/esm/styles/prism'; +import { materialLight, materialDark } from 'react-syntax-highlighter/dist/esm/styles/prism'; import { Button, FlexBox } from '@ui5/webcomponents-react'; import styles from './YamlViewer.module.css'; import { useToast } from '../../context/ToastContext.tsx'; import { useTranslation } from 'react-i18next'; -import { useThemeMode } from '../../lib/useThemeMode.ts'; +import { useIsDarkModePreferred } from '../../hooks/useIsDarkModePreferred.ts'; type YamlViewerProps = { yamlString: string; filename: string }; const YamlViewer: FC = ({ yamlString, filename }) => { const toast = useToast(); const { t } = useTranslation(); - const { isDarkMode } = useThemeMode(); + const isDarkModePreferred = useIsDarkModePreferred(); const copyToClipboard = () => { navigator.clipboard.writeText(yamlString); toast.show(t('yaml.copiedToClipboard')); @@ -33,13 +30,7 @@ const YamlViewer: FC = ({ yamlString, filename }) => { return (
- + @@ -49,7 +40,7 @@ const YamlViewer: FC = ({ yamlString, filename }) => { void) { + const mediaQueryList = window.matchMedia(mediaQuery); + mediaQueryList.addEventListener('change', callback); + + return () => { + mediaQueryList.removeEventListener('change', callback); + }; + } + + return function useMediaQuery() { + return useSyncExternalStore(subscribe, getSnapshot); + }; +} + +export const useIsDarkModePreferred = makeMediaQueryStore('(prefers-color-scheme: dark)'); diff --git a/src/lib/useThemeMode.ts b/src/lib/useThemeMode.ts deleted file mode 100644 index 5ed9c0e2..00000000 --- a/src/lib/useThemeMode.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const useThemeMode = (): { - isDarkMode: boolean; - mode: 'dark' | 'light'; -} => { - const isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches; - return { isDarkMode: isDarkMode, mode: isDarkMode ? 'dark' : 'light' }; -}; diff --git a/src/main.tsx b/src/main.tsx index 378b9cab..6c8c823c 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -7,7 +7,7 @@ import { ToastProvider } from './context/ToastContext.tsx'; import { CopyButtonProvider } from './context/CopyButtonContext.tsx'; import { FrontendConfigProvider } from './context/FrontendConfigContext.tsx'; import '@ui5/webcomponents-react/dist/Assets'; //used for loading themes -import { DarkModeSystemSwitcher } from './components/Core/DarkModeSystemSwitcher.tsx'; +import { ThemeManager } from './components/ThemeManager/ThemeManager.tsx'; import '.././i18n.ts'; import './utils/i18n/timeAgo'; import { ErrorBoundary, FallbackProps } from 'react-error-boundary'; @@ -49,7 +49,7 @@ export function createApp() { - + diff --git a/src/spaces/onboarding/auth/AuthContextOnboarding.tsx b/src/spaces/onboarding/auth/AuthContextOnboarding.tsx index 9091014a..9f05eb07 100644 --- a/src/spaces/onboarding/auth/AuthContextOnboarding.tsx +++ b/src/spaces/onboarding/auth/AuthContextOnboarding.tsx @@ -12,9 +12,7 @@ interface AuthContextOnboardingType { logout: () => Promise; } -const AuthContextOnboarding = createContext( - null, -); +const AuthContextOnboarding = createContext(null); export function AuthProviderOnboarding({ children }: { children: ReactNode }) { const [isAuthenticated, setIsAuthenticated] = useState(false); @@ -40,22 +38,16 @@ export function AuthProviderOnboarding({ children }: { children: ReactNode }) { } catch (_) { /* safe to ignore */ } - throw new Error( - errorBody?.message || - `Authentication check failed with status: ${response.status}`, - ); + throw new Error(errorBody?.message || `Authentication check failed with status: ${response.status}`); } const body = await response.json(); const validationResult = MeResponseSchema.safeParse(body); if (!validationResult.success) { - throw new Error( - `Auth API response validation failed: ${validationResult.error.flatten()}`, - ); + throw new Error(`Auth API response validation failed: ${validationResult.error.flatten()}`); } - const { isAuthenticated: apiIsAuthenticated, user: apiUser } = - validationResult.data; + const { isAuthenticated: apiIsAuthenticated, user: apiUser } = validationResult.data; setUser(apiUser); setIsAuthenticated(apiIsAuthenticated); @@ -75,10 +67,10 @@ export function AuthProviderOnboarding({ children }: { children: ReactNode }) { const login = () => { sessionStorage.setItem(AUTH_FLOW_SESSION_KEY, 'onboarding'); - - window.location.replace( - `/api/auth/onboarding/login?redirectTo=${encodeURIComponent(window.location.hash)}`, - ); + // The query parameters and hash fragments need to be preserved, e.g. /?sap-theme=sap_horizon#/mcp/projects + const { search, hash } = window.location; + const redirectTo = (search ? `/${search}` : '') + hash; + window.location.replace(`/api/auth/onboarding/login?redirectTo=${encodeURIComponent(redirectTo)}`); }; const logout = async () => { @@ -94,9 +86,7 @@ export function AuthProviderOnboarding({ children }: { children: ReactNode }) { } catch (_) { /* safe to ignore */ } - throw new Error( - errorBody?.message || `Logout failed with status: ${response.status}`, - ); + throw new Error(errorBody?.message || `Logout failed with status: ${response.status}`); } await refreshAuthStatus(); @@ -106,9 +96,7 @@ export function AuthProviderOnboarding({ children }: { children: ReactNode }) { }; return ( - + {children} ); @@ -117,9 +105,7 @@ export function AuthProviderOnboarding({ children }: { children: ReactNode }) { export const useAuthOnboarding = () => { const context = use(AuthContextOnboarding); if (!context) { - throw new Error( - 'useAuthOnboarding must be used within an AuthProviderOnboarding.', - ); + throw new Error('useAuthOnboarding must be used within an AuthProviderOnboarding.'); } return context; }; From cd33b4b8b277ddee909112b8afa8348103b45984 Mon Sep 17 00:00:00 2001 From: Andreas Kienle Date: Wed, 23 Jul 2025 15:55:38 +0200 Subject: [PATCH 2/3] Refactor --- src/components/ThemeManager.tsx | 13 +++++++ .../ThemeManager/ThemeManager.spec.ts | 21 ---------- src/components/ThemeManager/ThemeManager.tsx | 25 ------------ src/components/Yaml/YamlViewer.tsx | 6 +-- src/hooks/useIsDarkModePreferred.ts | 22 ----------- src/hooks/useTheme.ts | 38 +++++++++++++++++++ src/main.tsx | 2 +- 7 files changed, 55 insertions(+), 72 deletions(-) create mode 100644 src/components/ThemeManager.tsx delete mode 100644 src/components/ThemeManager/ThemeManager.spec.ts delete mode 100644 src/components/ThemeManager/ThemeManager.tsx delete mode 100644 src/hooks/useIsDarkModePreferred.ts create mode 100644 src/hooks/useTheme.ts diff --git a/src/components/ThemeManager.tsx b/src/components/ThemeManager.tsx new file mode 100644 index 00000000..e7e43dc1 --- /dev/null +++ b/src/components/ThemeManager.tsx @@ -0,0 +1,13 @@ +import { useEffect } from 'react'; +import { setTheme } from '@ui5/webcomponents-base/dist/config/Theme.js'; +import { useTheme } from '../hooks/useTheme.ts'; + +export function ThemeManager() { + const { theme } = useTheme(); + + useEffect(() => { + void setTheme(theme); + }, [theme]); + + return null; +} diff --git a/src/components/ThemeManager/ThemeManager.spec.ts b/src/components/ThemeManager/ThemeManager.spec.ts deleted file mode 100644 index 144b02ad..00000000 --- a/src/components/ThemeManager/ThemeManager.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { resolveTheme } from './ThemeManager.tsx'; - -describe('ThemeManager', () => { - describe('resolveTheme()', () => { - it('returns theme coming from URL when it is truthy', () => { - expect(resolveTheme('sap_fiori_3', true)).toBe('sap_fiori_3'); - expect(resolveTheme('custom_theme', false)).toBe('custom_theme'); - }); - - it('falls back to dark default when URL theme is falsy and user prefers dark mode', () => { - expect(resolveTheme(null, true)).toBe('sap_horizon_dark'); - expect(resolveTheme('', true)).toBe('sap_horizon_dark'); - }); - - it('falls back to light default when URL theme is falsy and user prefers light mode', () => { - expect(resolveTheme(null, false)).toBe('sap_horizon'); - expect(resolveTheme('', false)).toBe('sap_horizon'); - }); - }); -}); diff --git a/src/components/ThemeManager/ThemeManager.tsx b/src/components/ThemeManager/ThemeManager.tsx deleted file mode 100644 index a4f864f5..00000000 --- a/src/components/ThemeManager/ThemeManager.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { useEffect } from 'react'; -import { setTheme } from '@ui5/webcomponents-base/dist/config/Theme.js'; -import { useIsDarkModePreferred } from '../../hooks/useIsDarkModePreferred.ts'; - -const DEFAULT_THEME_LIGHT = 'sap_horizon'; -const DEFAULT_THEME_DARK = 'sap_horizon_dark'; - -export function resolveTheme(themeFromUrl: string | null, isDarkModePreferred: boolean): string { - if (themeFromUrl) { - return themeFromUrl; - } - return isDarkModePreferred ? DEFAULT_THEME_DARK : DEFAULT_THEME_LIGHT; -} - -export function ThemeManager() { - const isDarkModePreferred = useIsDarkModePreferred(); - const themeFromUrl = new URL(window.location.href).searchParams.get('sap-theme'); - - useEffect(() => { - const resolvedTheme = resolveTheme(themeFromUrl, isDarkModePreferred); - void setTheme(resolvedTheme); - }, [isDarkModePreferred, themeFromUrl]); - - return null; -} diff --git a/src/components/Yaml/YamlViewer.tsx b/src/components/Yaml/YamlViewer.tsx index 3828ddca..fa1cefa7 100644 --- a/src/components/Yaml/YamlViewer.tsx +++ b/src/components/Yaml/YamlViewer.tsx @@ -6,12 +6,12 @@ import { Button, FlexBox } from '@ui5/webcomponents-react'; import styles from './YamlViewer.module.css'; import { useToast } from '../../context/ToastContext.tsx'; import { useTranslation } from 'react-i18next'; -import { useIsDarkModePreferred } from '../../hooks/useIsDarkModePreferred.ts'; +import { useTheme } from '../../hooks/useTheme.ts'; type YamlViewerProps = { yamlString: string; filename: string }; const YamlViewer: FC = ({ yamlString, filename }) => { const toast = useToast(); const { t } = useTranslation(); - const isDarkModePreferred = useIsDarkModePreferred(); + const { isDarkTheme } = useTheme(); const copyToClipboard = () => { navigator.clipboard.writeText(yamlString); toast.show(t('yaml.copiedToClipboard')); @@ -40,7 +40,7 @@ const YamlViewer: FC = ({ yamlString, filename }) => { void) { - const mediaQueryList = window.matchMedia(mediaQuery); - mediaQueryList.addEventListener('change', callback); - - return () => { - mediaQueryList.removeEventListener('change', callback); - }; - } - - return function useMediaQuery() { - return useSyncExternalStore(subscribe, getSnapshot); - }; -} - -export const useIsDarkModePreferred = makeMediaQueryStore('(prefers-color-scheme: dark)'); diff --git a/src/hooks/useTheme.ts b/src/hooks/useTheme.ts new file mode 100644 index 00000000..3b1d0c98 --- /dev/null +++ b/src/hooks/useTheme.ts @@ -0,0 +1,38 @@ +import { useSyncExternalStore } from 'react'; +import { Theme } from '@ui5/webcomponents-react'; + +const DEFAULT_THEME_LIGHT = Theme.sap_horizon; +const DEFAULT_THEME_DARK = Theme.sap_horizon_dark; +const DARK_SAP_THEMES = new Set([ + Theme.sap_fiori_3_dark, + Theme.sap_fiori_3_hcb, + Theme.sap_horizon_dark, + Theme.sap_horizon_hcb, +]); + +function useSystemDarkModePreference() { + return useSyncExternalStore( + (callback) => { + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + mediaQuery.addEventListener('change', callback); + return () => mediaQuery.removeEventListener('change', callback); + }, + () => window.matchMedia('(prefers-color-scheme: dark)').matches, + ); +} + +export function useTheme() { + const systemPrefersDark = useSystemDarkModePreference(); + const themeFromUrl = new URL(window.location.href).searchParams.get('sap-theme'); + + // Theme from URL takes precedence over system settings + const theme = themeFromUrl || (systemPrefersDark ? DEFAULT_THEME_DARK : DEFAULT_THEME_LIGHT); + + // For well-defined SAP themes, return if they are light or dark – unknown themes will fall back to light + const isDarkTheme = DARK_SAP_THEMES.has(theme); + + return { + theme, + isDarkTheme, + }; +} diff --git a/src/main.tsx b/src/main.tsx index 6c8c823c..5ad8a947 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -7,7 +7,7 @@ import { ToastProvider } from './context/ToastContext.tsx'; import { CopyButtonProvider } from './context/CopyButtonContext.tsx'; import { FrontendConfigProvider } from './context/FrontendConfigContext.tsx'; import '@ui5/webcomponents-react/dist/Assets'; //used for loading themes -import { ThemeManager } from './components/ThemeManager/ThemeManager.tsx'; +import { ThemeManager } from './components/ThemeManager.tsx'; import '.././i18n.ts'; import './utils/i18n/timeAgo'; import { ErrorBoundary, FallbackProps } from 'react-error-boundary'; From fe770e23e563cb7e42778a727fb53c7c422a39ee Mon Sep 17 00:00:00 2001 From: Andreas Kienle Date: Wed, 23 Jul 2025 18:45:35 +0200 Subject: [PATCH 3/3] Preserve theme after redirect --- src/common/auth/getRedirectSuffix.spec.ts | 43 +++++++++++++++++++ src/common/auth/getRedirectSuffix.ts | 15 +++++++ src/lib/api/fetch.ts | 12 +++--- src/spaces/mcp/auth/AuthContextMcp.tsx | 21 +++------ .../onboarding/auth/AuthContextOnboarding.tsx | 6 +-- 5 files changed, 71 insertions(+), 26 deletions(-) create mode 100644 src/common/auth/getRedirectSuffix.spec.ts create mode 100644 src/common/auth/getRedirectSuffix.ts diff --git a/src/common/auth/getRedirectSuffix.spec.ts b/src/common/auth/getRedirectSuffix.spec.ts new file mode 100644 index 00000000..cc3b1345 --- /dev/null +++ b/src/common/auth/getRedirectSuffix.spec.ts @@ -0,0 +1,43 @@ +import { describe, it, expect, afterAll } from 'vitest'; +import { getRedirectSuffix } from './getRedirectSuffix'; + +const originalLocation = globalThis.location; + +function mockLocation(search: string, hash: string) { + Object.defineProperty(globalThis, 'location', { + value: { ...originalLocation, search, hash }, + writable: true, + configurable: true, + }); +} + +// Restore the real object once all tests have finished +afterAll(() => { + Object.defineProperty(globalThis, 'location', { + value: originalLocation, + writable: true, + configurable: true, + }); +}); + +describe('getRedirectSuffix()', () => { + it('returns "/{search}{hash}" when both parts are present', () => { + mockLocation('?sap-theme=sap_horizon', '#/mcp/projects'); + expect(getRedirectSuffix()).toBe('/?sap-theme=sap_horizon#/mcp/projects'); + }); + + it('returns "/{search}" when only the query string exists', () => { + mockLocation('?query=foo', ''); + expect(getRedirectSuffix()).toBe('/?query=foo'); + }); + + it('returns "{hash}" when only the hash fragment exists', () => { + mockLocation('', '#/dashboard'); + expect(getRedirectSuffix()).toBe('#/dashboard'); + }); + + it('returns an empty string when neither search nor hash exist', () => { + mockLocation('', ''); + expect(getRedirectSuffix()).toBe(''); + }); +}); diff --git a/src/common/auth/getRedirectSuffix.ts b/src/common/auth/getRedirectSuffix.ts new file mode 100644 index 00000000..cdf53bdc --- /dev/null +++ b/src/common/auth/getRedirectSuffix.ts @@ -0,0 +1,15 @@ +/** + * Generates the part of the URL (query string and hash fragments) that must be kept when redirecting the user. + * + * @example + * ```ts + * // Current URL: https://example.com/?sap-theme=sap_horizon#/mcp/projects + * + * const redirectTo = getRedirectSuffix(); + * // redirectTo -> "/?sap-theme=sap_horizon#/mcp/projects" + * ``` + */ +export function getRedirectSuffix() { + const { search, hash } = globalThis.location; + return (search ? `/${search}` : '') + hash; +} diff --git a/src/lib/api/fetch.ts b/src/lib/api/fetch.ts index 879f0820..4a27192e 100644 --- a/src/lib/api/fetch.ts +++ b/src/lib/api/fetch.ts @@ -1,5 +1,7 @@ import { APIError } from './error'; import { ApiConfig } from './types/apiConfig'; +import { AUTH_FLOW_SESSION_KEY } from '../../common/auth/AuthCallbackHandler.tsx'; +import { getRedirectSuffix } from '../../common/auth/getRedirectSuffix.ts'; const useCrateClusterHeader = 'X-use-crate'; const projectNameHeader = 'X-project'; @@ -48,13 +50,11 @@ export const fetchApiServer = async ( if (!res.ok) { if (res.status === 401) { - // Unauthorized, redirect to the login page - window.location.replace('/api/auth/onboarding/login'); + // Unauthorized (token expired), redirect to the login page + sessionStorage.setItem(AUTH_FLOW_SESSION_KEY, 'onboarding'); + window.location.replace(`/api/auth/onboarding/login?redirectTo=${encodeURIComponent(getRedirectSuffix())}`); } - const error = new APIError( - 'An error occurred while fetching the data.', - res.status, - ); + const error = new APIError('An error occurred while fetching the data.', res.status); error.info = await res.json(); throw error; } diff --git a/src/spaces/mcp/auth/AuthContextMcp.tsx b/src/spaces/mcp/auth/AuthContextMcp.tsx index 58759c63..e728f8a2 100644 --- a/src/spaces/mcp/auth/AuthContextMcp.tsx +++ b/src/spaces/mcp/auth/AuthContextMcp.tsx @@ -1,6 +1,7 @@ import { createContext, useState, useEffect, ReactNode, use } from 'react'; import { MeResponseSchema } from './auth.schemas'; import { AUTH_FLOW_SESSION_KEY } from '../../../common/auth/AuthCallbackHandler.tsx'; +import { getRedirectSuffix } from '../../../common/auth/getRedirectSuffix.ts'; interface AuthContextMcpType { isLoading: boolean; @@ -34,18 +35,13 @@ export function AuthProviderMcp({ children }: { children: ReactNode }) { } catch (_) { /* safe to ignore */ } - throw new Error( - errorBody?.message || - `Authentication check failed with status: ${response.status}`, - ); + throw new Error(errorBody?.message || `Authentication check failed with status: ${response.status}`); } const body = await response.json(); const validationResult = MeResponseSchema.safeParse(body); if (!validationResult.success) { - throw new Error( - `Auth API response validation failed: ${validationResult.error.flatten()}`, - ); + throw new Error(`Auth API response validation failed: ${validationResult.error.flatten()}`); } const { isAuthenticated: apiIsAuthenticated } = validationResult.data; @@ -60,17 +56,10 @@ export function AuthProviderMcp({ children }: { children: ReactNode }) { const login = () => { sessionStorage.setItem(AUTH_FLOW_SESSION_KEY, 'mcp'); - - window.location.replace( - `/api/auth/mcp/login?redirectTo=${encodeURIComponent(window.location.hash)}`, - ); + window.location.replace(`/api/auth/mcp/login?redirectTo=${encodeURIComponent(getRedirectSuffix())}`); }; - return ( - - {children} - - ); + return {children}; } export const useAuthMcp = () => { diff --git a/src/spaces/onboarding/auth/AuthContextOnboarding.tsx b/src/spaces/onboarding/auth/AuthContextOnboarding.tsx index 9f05eb07..4efe7a0c 100644 --- a/src/spaces/onboarding/auth/AuthContextOnboarding.tsx +++ b/src/spaces/onboarding/auth/AuthContextOnboarding.tsx @@ -2,6 +2,7 @@ import { createContext, useState, useEffect, ReactNode, use } from 'react'; import { MeResponseSchema, User } from './auth.schemas'; import { AUTH_FLOW_SESSION_KEY } from '../../../common/auth/AuthCallbackHandler.tsx'; import * as Sentry from '@sentry/react'; +import { getRedirectSuffix } from '../../../common/auth/getRedirectSuffix.ts'; interface AuthContextOnboardingType { isLoading: boolean; @@ -67,10 +68,7 @@ export function AuthProviderOnboarding({ children }: { children: ReactNode }) { const login = () => { sessionStorage.setItem(AUTH_FLOW_SESSION_KEY, 'onboarding'); - // The query parameters and hash fragments need to be preserved, e.g. /?sap-theme=sap_horizon#/mcp/projects - const { search, hash } = window.location; - const redirectTo = (search ? `/${search}` : '') + hash; - window.location.replace(`/api/auth/onboarding/login?redirectTo=${encodeURIComponent(redirectTo)}`); + window.location.replace(`/api/auth/onboarding/login?redirectTo=${encodeURIComponent(getRedirectSuffix())}`); }; const logout = async () => {