From f8386843d46f0363026c839306a60250373bdc48 Mon Sep 17 00:00:00 2001 From: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com> Date: Tue, 25 Nov 2025 18:45:40 +0000 Subject: [PATCH] Bug/v2/onboarding slides fix (#5005) # Description of Changes - Stop onboarding from appearing before logging in - Also (should've done this in a different PR) fixed a small bug in settings when changing logo --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Co-authored-by: Connor Yoh --- .../InitialOnboardingModal/index.tsx | 2 +- .../onboarding/hooks/useOnboardingFlow.ts | 34 +++++++++++++++++-- .../proprietary/components/AppProviders.tsx | 2 +- .../configSections/AdminGeneralSection.tsx | 26 +++++++++++--- .../configSections/AdminPlanSection.tsx | 1 + 5 files changed, 55 insertions(+), 10 deletions(-) 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 (
- Stirling icon + Stirling icon
); 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" /> )} +