diff --git a/frontend/src/core/components/onboarding/InitialOnboardingModal/index.tsx b/frontend/src/core/components/onboarding/InitialOnboardingModal/index.tsx
index 0ab33cfed..0d8214270 100644
--- a/frontend/src/core/components/onboarding/InitialOnboardingModal/index.tsx
+++ b/frontend/src/core/components/onboarding/InitialOnboardingModal/index.tsx
@@ -34,7 +34,7 @@ export default function InitialOnboardingModal(props: InitialOnboardingModalProp
return (
-

+
);
diff --git a/frontend/src/core/components/onboarding/hooks/useOnboardingFlow.ts b/frontend/src/core/components/onboarding/hooks/useOnboardingFlow.ts
index 18dec59fc..b2b97b2a3 100644
--- a/frontend/src/core/components/onboarding/hooks/useOnboardingFlow.ts
+++ b/frontend/src/core/components/onboarding/hooks/useOnboardingFlow.ts
@@ -4,7 +4,7 @@ import { useAppConfig } from '@app/contexts/AppConfigContext';
import { useCookieConsentContext } from '@app/contexts/CookieConsentContext';
import { useOnboarding } from '@app/contexts/OnboardingContext';
import type { LicenseNotice } from '@app/types/types';
-import { useNavigate } from 'react-router-dom';
+import { useNavigate, useLocation } from 'react-router-dom';
import {
ONBOARDING_SESSION_BLOCK_KEY,
ONBOARDING_SESSION_EVENT,
@@ -13,6 +13,15 @@ import {
} from '@app/constants/events';
import { useServerExperience } from '@app/hooks/useServerExperience';
+// Auth routes where onboarding should NOT show
+const AUTH_ROUTES = ['/login', '/signup', '/auth', '/invite'];
+
+// Check if user has an auth token (to avoid flash before redirect)
+function hasAuthToken(): boolean {
+ if (typeof window === 'undefined') return false;
+ return !!localStorage.getItem('stirling_jwt');
+}
+
interface InitialModalHandlers {
opened: boolean;
onLicenseNoticeUpdate: (notice: LicenseNotice) => void;
@@ -29,11 +38,30 @@ interface ServerLicenseModalHandlers {
export function useOnboardingFlow() {
const { preferences, updatePreference } = usePreferences();
- const { config } = useAppConfig();
+ const { config, loading: configLoading } = useAppConfig();
const { showCookieConsent, isReady: isCookieConsentReady } = useCookieConsentContext();
const { completeTour, tourType, isOpen } = useOnboarding();
+ const location = useLocation();
- const shouldShowIntro = !preferences.hasSeenIntroOnboarding;
+ // Check if we're on an auth route (login, signup, etc.)
+ const isOnAuthRoute = AUTH_ROUTES.some(route => location.pathname.startsWith(route));
+
+ // Check if login is enabled but user doesn't have a token
+ // This prevents a flash of the modal before redirect to /login
+ const loginEnabled = config?.enableLogin === true;
+ const isUnauthenticatedWithLoginEnabled = loginEnabled && !hasAuthToken();
+
+ // Don't show intro onboarding:
+ // 1. On explicit auth routes (/login, /signup, etc.)
+ // 2. While config is still loading
+ // 3. When login is enabled but user isn't authenticated (would redirect to /login)
+ // This ensures:
+ // - If login is enabled: user must be logged in before seeing onboarding
+ // - If login is disabled: homepage must have rendered first
+ const shouldShowIntro = !preferences.hasSeenIntroOnboarding
+ && !isOnAuthRoute
+ && !configLoading
+ && !isUnauthenticatedWithLoginEnabled;
const isAdminUser = !!config?.isAdmin;
const { hasPaidLicense } = useServerExperience();
diff --git a/frontend/src/proprietary/components/AppProviders.tsx b/frontend/src/proprietary/components/AppProviders.tsx
index e92cd5573..a0e9a63dc 100644
--- a/frontend/src/proprietary/components/AppProviders.tsx
+++ b/frontend/src/proprietary/components/AppProviders.tsx
@@ -2,9 +2,9 @@ import { AppProviders as CoreAppProviders, AppProvidersProps } from "@core/compo
import { AuthProvider } from "@app/auth/UseSession";
import { LicenseProvider } from "@app/contexts/LicenseContext";
import { CheckoutProvider } from "@app/contexts/CheckoutContext";
-import { UpdateSeatsProvider } from "@app/contexts/UpdateSeatsContext"
import { UpgradeBannerInitializer } from "@app/components/shared/UpgradeBannerInitializer";
import { ServerExperienceProvider } from "@app/contexts/ServerExperienceContext";
+import { UpdateSeatsProvider } from "@app/contexts/UpdateSeatsContext";
export function AppProviders({ children, appConfigRetryOptions, appConfigProviderProps }: AppProvidersProps) {
return (
diff --git a/frontend/src/proprietary/components/shared/config/configSections/AdminGeneralSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminGeneralSection.tsx
index b3bc6c99d..14eda5f9e 100644
--- a/frontend/src/proprietary/components/shared/config/configSections/AdminGeneralSection.tsx
+++ b/frontend/src/proprietary/components/shared/config/configSections/AdminGeneralSection.tsx
@@ -54,6 +54,7 @@ export default function AdminGeneralSection() {
const [originalSettingsSnapshot, setOriginalSettingsSnapshot] = useState('');
const [isDirty, setLocalIsDirty] = useState(false);
const isInitialLoad = useRef(true);
+ const justSavedRef = useRef(false);
const {
settings,
@@ -158,9 +159,12 @@ export default function AdminGeneralSection() {
}
}, [loginEnabled, fetchSettings]);
- // Snapshot original settings after initial load and sync local preference with server
+ // Snapshot original settings after initial load OR after successful save (when refetch completes)
useEffect(() => {
- if (!loading && isInitialLoad.current && Object.keys(settings).length > 0) {
+ if (loading || Object.keys(settings).length === 0) return;
+
+ // After initial load: set snapshot and sync preference
+ if (isInitialLoad.current) {
setOriginalSettingsSnapshot(JSON.stringify(settings));
// Sync local preference with server setting on initial load to ensure they're in sync
@@ -170,8 +174,17 @@ export default function AdminGeneralSection() {
}
isInitialLoad.current = false;
+ return;
}
- }, [loading, settings, loginEnabled, updatePreference]);
+
+ // After save: update snapshot to new server state so dirty tracking is accurate
+ if (justSavedRef.current) {
+ setOriginalSettingsSnapshot(JSON.stringify(settings));
+ setLocalIsDirty(false);
+ setIsDirty(false);
+ justSavedRef.current = false;
+ }
+ }, [loading, settings, loginEnabled, updatePreference, setIsDirty]);
// Track dirty state by comparing current settings to snapshot
useEffect(() => {
@@ -238,6 +251,9 @@ export default function AdminGeneralSection() {
}
try {
+ // Mark that we just saved - the snapshot will be updated when refetch completes
+ justSavedRef.current = true;
+
await saveSettings();
// Update local preference after successful save so the app reflects the saved logo style
@@ -245,12 +261,12 @@ export default function AdminGeneralSection() {
updatePreference('logoVariant', settings.ui.logoStyle);
}
- // Update snapshot to current settings after successful save
- setOriginalSettingsSnapshot(JSON.stringify(settings));
+ // Clear dirty state immediately (snapshot will be updated by effect when refetch completes)
setLocalIsDirty(false);
markClean();
showRestartModal();
} catch (_error) {
+ justSavedRef.current = false;
alert({
alertType: 'error',
title: t('admin.error', 'Error'),
diff --git a/frontend/src/proprietary/components/shared/config/configSections/AdminPlanSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminPlanSection.tsx
index c5d1ff658..f413f8a50 100644
--- a/frontend/src/proprietary/components/shared/config/configSections/AdminPlanSection.tsx
+++ b/frontend/src/proprietary/components/shared/config/configSections/AdminPlanSection.tsx
@@ -222,6 +222,7 @@ const AdminPlanSection: React.FC = () => {
buttonColor="orange.7"
/>
)}
+