mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
Chore/v2/onboarding flow cleanup (#5065)
This commit is contained in:
@@ -8,7 +8,7 @@ import Login from "@app/routes/Login";
|
||||
import Signup from "@app/routes/Signup";
|
||||
import AuthCallback from "@app/routes/AuthCallback";
|
||||
import InviteAccept from "@app/routes/InviteAccept";
|
||||
import OnboardingTour from "@app/components/onboarding/OnboardingTour";
|
||||
import Onboarding from "@app/components/onboarding/Onboarding";
|
||||
|
||||
// Import global styles
|
||||
import "@app/styles/tailwind.css";
|
||||
@@ -34,7 +34,7 @@ export default function App() {
|
||||
{/* Main app routes - Landing handles auth logic */}
|
||||
<Route path="/*" element={<Landing />} />
|
||||
</Routes>
|
||||
<OnboardingTour />
|
||||
<Onboarding />
|
||||
</AppLayout>
|
||||
</AppProviders>
|
||||
</Suspense>
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useOnboarding } from '@app/contexts/OnboardingContext';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { isAuthRoute } from '@core/constants/routes';
|
||||
import { useCheckout } from '@app/contexts/CheckoutContext';
|
||||
import { InfoBanner } from '@app/components/shared/InfoBanner';
|
||||
import {
|
||||
ONBOARDING_SESSION_BLOCK_KEY,
|
||||
ONBOARDING_SESSION_EVENT,
|
||||
SERVER_LICENSE_REQUEST_EVENT,
|
||||
type ServerLicenseRequestPayload,
|
||||
UPGRADE_BANNER_TEST_EVENT,
|
||||
@@ -15,13 +13,17 @@ import {
|
||||
UPGRADE_BANNER_ALERT_EVENT,
|
||||
} from '@core/constants/events';
|
||||
import { useServerExperience } from '@app/hooks/useServerExperience';
|
||||
import { hasSeenStep } from '@core/components/onboarding/orchestrator/onboardingStorage';
|
||||
|
||||
const FRIENDLY_LAST_SEEN_KEY = 'upgradeBannerFriendlyLastShownAt';
|
||||
const WEEK_IN_MS = 7 * 24 * 60 * 60 * 1000;
|
||||
const UpgradeBanner: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { isOpen: tourOpen } = useOnboarding();
|
||||
const location = useLocation();
|
||||
|
||||
// Check if we're on an auth route (evaluated after hooks, used in render)
|
||||
const onAuthRoute = isAuthRoute(location.pathname);
|
||||
const { openCheckout } = useCheckout();
|
||||
const {
|
||||
totalUsers,
|
||||
@@ -34,40 +36,18 @@ const UpgradeBanner: React.FC = () => {
|
||||
overFreeTierLimit,
|
||||
scenarioKey,
|
||||
} = useServerExperience();
|
||||
const [sessionBlocked, setSessionBlocked] = useState(true);
|
||||
const [friendlyVisible, setFriendlyVisible] = useState(false);
|
||||
const onboardingComplete = hasSeenStep('welcome');
|
||||
const [friendlyVisible, setFriendlyVisible] = useState(() => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
const lastShownRaw = window.localStorage.getItem(FRIENDLY_LAST_SEEN_KEY);
|
||||
if (!lastShownRaw) return false;
|
||||
const lastShown = parseInt(lastShownRaw, 10);
|
||||
if (!Number.isFinite(lastShown)) return false;
|
||||
return Date.now() - lastShown >= WEEK_IN_MS;
|
||||
});
|
||||
const isDev = import.meta.env.DEV;
|
||||
const [testScenario, setTestScenario] = useState<UpgradeBannerTestScenario>(null);
|
||||
|
||||
// Track onboarding session flag so we don't show banner if onboarding ran this load
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const evaluateBlock = () => {
|
||||
const blocked = window.sessionStorage.getItem(ONBOARDING_SESSION_BLOCK_KEY) === 'true';
|
||||
setSessionBlocked(blocked);
|
||||
};
|
||||
|
||||
evaluateBlock();
|
||||
|
||||
const timer = window.setTimeout(() => {
|
||||
evaluateBlock();
|
||||
}, 1000);
|
||||
|
||||
const handleOnboardingEvent = () => {
|
||||
evaluateBlock();
|
||||
};
|
||||
|
||||
window.addEventListener(ONBOARDING_SESSION_EVENT, handleOnboardingEvent as EventListener);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
window.removeEventListener(ONBOARDING_SESSION_EVENT, handleOnboardingEvent as EventListener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDev || typeof window === 'undefined') {
|
||||
return;
|
||||
@@ -157,28 +137,16 @@ const UpgradeBanner: React.FC = () => {
|
||||
shouldShowFriendlyBase &&
|
||||
!licenseLoading &&
|
||||
effectiveTotalUsersLoaded &&
|
||||
!tourOpen &&
|
||||
!sessionBlocked,
|
||||
onboardingComplete,
|
||||
);
|
||||
// Urgent banner should always show when over-limit
|
||||
const shouldEvaluateUrgent = scenario
|
||||
? Boolean(scenario && !scenarioIsFriendly)
|
||||
: Boolean(
|
||||
shouldShowUrgentBase &&
|
||||
!licenseLoading &&
|
||||
!tourOpen &&
|
||||
!sessionBlocked,
|
||||
!licenseLoading,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!shouldShowFriendlyBase && effectiveTotalUsersLoaded) {
|
||||
window.localStorage.removeItem(FRIENDLY_LAST_SEEN_KEY);
|
||||
}
|
||||
}, [shouldShowFriendlyBase, effectiveTotalUsersLoaded]);
|
||||
|
||||
useEffect(() => {
|
||||
if (scenario === 'friendly') {
|
||||
return;
|
||||
@@ -214,16 +182,6 @@ const UpgradeBanner: React.FC = () => {
|
||||
}
|
||||
: { active: false };
|
||||
|
||||
console.debug('[UpgradeBanner] Dispatching alert event', {
|
||||
shouldEvaluateUrgent,
|
||||
detail,
|
||||
totalUsers: effectiveTotalUsers,
|
||||
freeTierLimit,
|
||||
effectiveIsAdmin,
|
||||
effectiveHasPaidLicense,
|
||||
userCountLoaded: effectiveTotalUsersLoaded,
|
||||
});
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(UPGRADE_BANNER_ALERT_EVENT, { detail }),
|
||||
);
|
||||
@@ -246,12 +204,6 @@ const UpgradeBanner: React.FC = () => {
|
||||
window.localStorage.setItem(FRIENDLY_LAST_SEEN_KEY, Date.now().toString());
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (friendlyVisible) {
|
||||
recordFriendlyLastShown();
|
||||
}
|
||||
}, [friendlyVisible, recordFriendlyLastShown]);
|
||||
|
||||
const handleUpgrade = () => {
|
||||
recordFriendlyLastShown();
|
||||
|
||||
@@ -308,15 +260,8 @@ const UpgradeBanner: React.FC = () => {
|
||||
|
||||
const renderUrgentBanner = () => {
|
||||
if (!shouldEvaluateUrgent) {
|
||||
console.debug('[UpgradeBanner] renderUrgentBanner → hidden (shouldEvaluateUrgent=false)');
|
||||
return null;
|
||||
}
|
||||
console.debug('[UpgradeBanner] renderUrgentBanner → visible', {
|
||||
totalUsers: effectiveTotalUsers,
|
||||
freeTierLimit,
|
||||
effectiveIsAdmin,
|
||||
effectiveHasPaidLicense,
|
||||
});
|
||||
|
||||
const buttonText = effectiveIsAdmin ? t('upgradeBanner.seeInfo', 'See info') : undefined;
|
||||
|
||||
@@ -351,7 +296,8 @@ const UpgradeBanner: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
if (!friendlyVisible && !shouldEvaluateUrgent) {
|
||||
// Don't show on auth routes or if neither banner type should show
|
||||
if (onAuthRoute || (!friendlyVisible && !shouldEvaluateUrgent)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -173,19 +173,8 @@ export function ServerExperienceProvider({ children }: { children: ReactNode })
|
||||
}
|
||||
|
||||
const shouldUseAdminData = (config.enableLogin ?? true) && config.isAdmin;
|
||||
const shouldUseEstimate = config.enableLogin === false;
|
||||
|
||||
if (!shouldUseAdminData && !shouldUseEstimate) {
|
||||
setUserCountState((prev) => ({
|
||||
...prev,
|
||||
totalUsers: null,
|
||||
weeklyActiveUsers: null,
|
||||
loading: false,
|
||||
source: 'unknown',
|
||||
error: null,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
// Use WAU estimate for no-login scenarios OR for login non-admin users
|
||||
const shouldUseEstimate = config.enableLogin === false || !config.isAdmin;
|
||||
|
||||
setUserCountState((prev) => ({
|
||||
...prev,
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import { Navigate, useLocation, useNavigate } from 'react-router-dom'
|
||||
import { useAuth } from '@app/auth/UseSession'
|
||||
import { useAppConfig } from '@app/contexts/AppConfigContext'
|
||||
import HomePage from '@app/pages/HomePage'
|
||||
// Login component is used via routing, not directly imported
|
||||
import FirstLoginModal from '@app/components/shared/FirstLoginModal'
|
||||
import { accountService } from '@app/services/accountService'
|
||||
import { useBackendProbe } from '@app/hooks/useBackendProbe'
|
||||
import AuthLayout from '@app/routes/authShared/AuthLayout'
|
||||
import LoginHeader from '@app/routes/login/LoginHeader'
|
||||
@@ -19,15 +16,12 @@ import { useTranslation } from 'react-i18next'
|
||||
* If user is not authenticated: Show Login or redirect to /login
|
||||
*/
|
||||
export default function Landing() {
|
||||
const { session, loading: authLoading, refreshSession } = useAuth();
|
||||
const { session, loading: authLoading } = useAuth();
|
||||
const { config, loading: configLoading, refetch } = useAppConfig();
|
||||
const backendProbe = useBackendProbe();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const [isFirstLogin, setIsFirstLogin] = useState(false);
|
||||
const [checkingFirstLogin, setCheckingFirstLogin] = useState(false);
|
||||
const [username, setUsername] = useState('');
|
||||
|
||||
const loading = authLoading || configLoading || backendProbe.loading;
|
||||
|
||||
@@ -51,42 +45,12 @@ export default function Landing() {
|
||||
return () => window.clearInterval(intervalId);
|
||||
}, [backendProbe.status, backendProbe.loginDisabled, backendProbe.probe, navigate, refetch]);
|
||||
|
||||
// Check if user needs to change password on first login
|
||||
useEffect(() => {
|
||||
const checkFirstLogin = async () => {
|
||||
if (session && config?.enableLogin !== false) {
|
||||
try {
|
||||
setCheckingFirstLogin(true)
|
||||
const accountData = await accountService.getAccountData()
|
||||
setUsername(accountData.username)
|
||||
setIsFirstLogin(accountData.changeCredsFlag)
|
||||
} catch (err) {
|
||||
console.error('Failed to check first login status:', err)
|
||||
// If account endpoint fails (404), user probably doesn't have security enabled
|
||||
setIsFirstLogin(false)
|
||||
} finally {
|
||||
setCheckingFirstLogin(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkFirstLogin()
|
||||
}, [session, config])
|
||||
|
||||
useEffect(() => {
|
||||
if (backendProbe.status === 'up') {
|
||||
void refetch();
|
||||
}
|
||||
}, [backendProbe.status, refetch]);
|
||||
|
||||
const handlePasswordChanged = async () => {
|
||||
// After password change, backend logs out the user
|
||||
// Refresh session to detect logout and redirect to login
|
||||
setIsFirstLogin(false) // Close modal first
|
||||
await refreshSession()
|
||||
// The auth system will automatically redirect to login when session is null
|
||||
}
|
||||
|
||||
console.log('[Landing] State:', {
|
||||
pathname: location.pathname,
|
||||
loading,
|
||||
@@ -95,7 +59,7 @@ export default function Landing() {
|
||||
});
|
||||
|
||||
// Show loading while checking auth and config
|
||||
if (loading || checkingFirstLogin) {
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div className="text-center">
|
||||
@@ -154,17 +118,9 @@ export default function Landing() {
|
||||
}
|
||||
|
||||
// If we have a session, show the main app
|
||||
// Note: First login password change is now handled by the onboarding flow
|
||||
if (session) {
|
||||
return (
|
||||
<>
|
||||
<FirstLoginModal
|
||||
opened={isFirstLogin}
|
||||
onPasswordChanged={handlePasswordChanged}
|
||||
username={username}
|
||||
/>
|
||||
<HomePage />
|
||||
</>
|
||||
);
|
||||
return <HomePage />;
|
||||
}
|
||||
|
||||
// No session - redirect to login page
|
||||
|
||||
@@ -7,7 +7,6 @@ import Login from '@app/routes/Login';
|
||||
import { useAuth } from '@app/auth/UseSession';
|
||||
import { springAuth } from '@app/auth/springAuthClient';
|
||||
import { PreferencesProvider } from '@app/contexts/PreferencesContext';
|
||||
import { OnboardingProvider } from '@app/contexts/OnboardingContext';
|
||||
|
||||
// Mock i18n to return fallback text
|
||||
vi.mock('react-i18next', () => ({
|
||||
@@ -67,7 +66,7 @@ vi.mock('react-router-dom', async () => {
|
||||
const TestWrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<MantineProvider>
|
||||
<PreferencesProvider>
|
||||
<OnboardingProvider>{children}</OnboardingProvider>
|
||||
{children}
|
||||
</PreferencesProvider>
|
||||
</MantineProvider>
|
||||
);
|
||||
|
||||
@@ -129,8 +129,12 @@ const SIMULATION_SCENARIOS: SimulationScenario[] = [
|
||||
...BASE_LOGIN_CONFIG,
|
||||
isAdmin: false,
|
||||
},
|
||||
adminUsage: {
|
||||
totalUsers: 3,
|
||||
// Non-admin users use WAU estimate (not adminUsage)
|
||||
wau: {
|
||||
trackingSince: '2025-11-18T23:20:12.520884200Z',
|
||||
daysOnline: 0,
|
||||
totalUniqueBrowsers: 3,
|
||||
weeklyActiveUsers: 3,
|
||||
},
|
||||
licenseInfo: { ...FREE_LICENSE_INFO },
|
||||
},
|
||||
@@ -151,8 +155,12 @@ const SIMULATION_SCENARIOS: SimulationScenario[] = [
|
||||
...BASE_LOGIN_CONFIG,
|
||||
isAdmin: false,
|
||||
},
|
||||
adminUsage: {
|
||||
totalUsers: 12,
|
||||
// Non-admin users use WAU estimate (not adminUsage)
|
||||
wau: {
|
||||
trackingSince: '2025-09-01T00:00:00Z',
|
||||
daysOnline: 30,
|
||||
totalUniqueBrowsers: 12,
|
||||
weeklyActiveUsers: 9,
|
||||
},
|
||||
licenseInfo: { ...FREE_LICENSE_INFO },
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user