mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-14 03:30:19 +00:00
🐛 fix(desktop): resolve onboarding navigation issues after logout (#11628)
* 🐛 fix(desktop): resolve onboarding navigation issues after logout - Refactor step-based navigation to screen-based navigation system - Add DesktopOnboardingScreen enum for type-safe screen handling - Fix screen persistence and URL synchronization - Improve platform-specific screen resolution (macOS permissions) - Extract navigation logic into reusable utility functions * cleanup Signed-off-by: Innei <tukon479@gmail.com> * 🐛 fix(UserPanel): handle errors during remote server config clearance - Added error handling for the remote server configuration clearance process in the UserPanel component. - Ensured that the onboarding completion and sign-out actions are executed regardless of the error state. Signed-off-by: Innei <tukon479@gmail.com> --------- Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
@@ -21,7 +21,9 @@ async function generateJwks() {
|
|||||||
console.error('正在生成 RSA 密钥对...');
|
console.error('正在生成 RSA 密钥对...');
|
||||||
|
|
||||||
// 生成 RS256 密钥对
|
// 生成 RS256 密钥对
|
||||||
const { privateKey } = await generateKeyPair('RS256');
|
const { privateKey } = await generateKeyPair('RS256', {
|
||||||
|
extractable: true,
|
||||||
|
});
|
||||||
|
|
||||||
// 导出为 JWK 格式
|
// 导出为 JWK 格式
|
||||||
const jwk = await exportJWK(privateKey);
|
const jwk = await exportJWK(privateKey);
|
||||||
|
|||||||
@@ -199,10 +199,13 @@ const LoginStep = memo<LoginStepProps>(({ onBack, onNext }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleCancelAuth = async () => {
|
const handleCancelAuth = async () => {
|
||||||
await remoteServerService.cancelAuthorization();
|
setRemoteError(null);
|
||||||
|
clearRemoteServerSyncError();
|
||||||
|
|
||||||
setCloudLoginStatus('idle');
|
setCloudLoginStatus('idle');
|
||||||
setSelfhostLoginStatus('idle');
|
setSelfhostLoginStatus('idle');
|
||||||
setAuthProgress(null);
|
setAuthProgress(null);
|
||||||
|
await remoteServerService.cancelAuthorization();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 渲染 Cloud 登录内容
|
// 渲染 Cloud 登录内容
|
||||||
@@ -238,10 +241,9 @@ const LoginStep = memo<LoginStepProps>(({ onBack, onNext }) => {
|
|||||||
|
|
||||||
if (cloudLoginStatus === 'error') {
|
if (cloudLoginStatus === 'error') {
|
||||||
return (
|
return (
|
||||||
<>
|
<Flexbox style={{ width: '100%' }}>
|
||||||
<Alert
|
<Alert
|
||||||
description={remoteError || t('authResult.failed.desc')}
|
description={remoteError || t('authResult.failed.desc')}
|
||||||
style={{ width: '100%' }}
|
|
||||||
title={t('authResult.failed.title')}
|
title={t('authResult.failed.title')}
|
||||||
type={'secondary'}
|
type={'secondary'}
|
||||||
/>
|
/>
|
||||||
@@ -254,7 +256,7 @@ const LoginStep = memo<LoginStepProps>(({ onBack, onNext }) => {
|
|||||||
>
|
>
|
||||||
{t('screen5.actions.tryAgain')}
|
{t('screen5.actions.tryAgain')}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</Flexbox>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,10 +342,9 @@ const LoginStep = memo<LoginStepProps>(({ onBack, onNext }) => {
|
|||||||
|
|
||||||
if (selfhostLoginStatus === 'error') {
|
if (selfhostLoginStatus === 'error') {
|
||||||
return (
|
return (
|
||||||
<Flexbox gap={16}>
|
<Flexbox gap={16} style={{ width: '100%' }}>
|
||||||
<Alert
|
<Alert
|
||||||
description={remoteError || t('authResult.failed.desc')}
|
description={remoteError || t('authResult.failed.desc')}
|
||||||
style={{ width: '100%' }}
|
|
||||||
title={t('authResult.failed.title')}
|
title={t('authResult.failed.title')}
|
||||||
type={'secondary'}
|
type={'secondary'}
|
||||||
/>
|
/>
|
||||||
@@ -354,6 +355,47 @@ const LoginStep = memo<LoginStepProps>(({ onBack, onNext }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (selfhostLoginStatus === 'loading') {
|
||||||
|
const phaseText = t(authorizationPhaseI18nKeyMap[authProgress?.phase ?? 'browser_opened'], {
|
||||||
|
defaultValue: t('screen5.actions.connecting'),
|
||||||
|
});
|
||||||
|
const remainingSeconds = authProgress
|
||||||
|
? Math.max(0, Math.ceil((authProgress.maxPollTime - authProgress.elapsed) / 1000))
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flexbox gap={8} style={{ width: '100%' }}>
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
disabled={true}
|
||||||
|
icon={Server}
|
||||||
|
loading={true}
|
||||||
|
size={'large'}
|
||||||
|
type={'primary'}
|
||||||
|
>
|
||||||
|
{t('screen5.actions.connecting')}
|
||||||
|
</Button>
|
||||||
|
<Text style={{ color: cssVar.colorTextDescription }} type={'secondary'}>
|
||||||
|
{phaseText}
|
||||||
|
</Text>
|
||||||
|
<Flexbox align={'center'} horizontal justify={'space-between'}>
|
||||||
|
{remainingSeconds !== null ? (
|
||||||
|
<Text style={{ color: cssVar.colorTextDescription }} type={'secondary'}>
|
||||||
|
{t('screen5.auth.remaining', {
|
||||||
|
time: remainingSeconds,
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)}
|
||||||
|
<Button onClick={handleCancelAuth} size={'small'} type={'text'}>
|
||||||
|
{t('screen5.actions.cancel')}
|
||||||
|
</Button>
|
||||||
|
</Flexbox>
|
||||||
|
</Flexbox>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flexbox gap={16} style={{ width: '100%' }}>
|
<Flexbox gap={16} style={{ width: '100%' }}>
|
||||||
<Text color={cssVar.colorTextSecondary}>{t(loginMethodMetas.selfhost.descriptionKey)}</Text>
|
<Text color={cssVar.colorTextSecondary}>{t(loginMethodMetas.selfhost.descriptionKey)}</Text>
|
||||||
@@ -365,6 +407,11 @@ const LoginStep = memo<LoginStepProps>(({ onBack, onNext }) => {
|
|||||||
const { electronSystemService } = await import('@/services/electron/system');
|
const { electronSystemService } = await import('@/services/electron/system');
|
||||||
await electronSystemService.showContextMenu('edit');
|
await electronSystemService.showContextMenu('edit');
|
||||||
}}
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
handleSelfhostConnect();
|
||||||
|
}
|
||||||
|
}}
|
||||||
placeholder={t('screen5.selfhost.endpointPlaceholder')}
|
placeholder={t('screen5.selfhost.endpointPlaceholder')}
|
||||||
prefix={<Icon icon={Server} style={{ marginRight: 4 }} />}
|
prefix={<Icon icon={Server} style={{ marginRight: 4 }} />}
|
||||||
size={'large'}
|
size={'large'}
|
||||||
@@ -372,16 +419,14 @@ const LoginStep = memo<LoginStepProps>(({ onBack, onNext }) => {
|
|||||||
value={endpoint}
|
value={endpoint}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
disabled={!endpoint.trim() || selfhostLoginStatus === 'loading' || isConnectingServer}
|
disabled={!endpoint.trim() || isConnectingServer}
|
||||||
loading={selfhostLoginStatus === 'loading'}
|
loading={false}
|
||||||
onClick={handleSelfhostConnect}
|
onClick={handleSelfhostConnect}
|
||||||
size={'large'}
|
size={'large'}
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
type={'primary'}
|
type={'primary'}
|
||||||
>
|
>
|
||||||
{selfhostLoginStatus === 'loading'
|
{t('screen5.actions.connectToServer')}
|
||||||
? t('screen5.actions.connecting')
|
|
||||||
: t('screen5.actions.connectToServer')}
|
|
||||||
</Button>
|
</Button>
|
||||||
</Flexbox>
|
</Flexbox>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,40 +14,79 @@ import LoginStep from './features/LoginStep';
|
|||||||
import PermissionsStep from './features/PermissionsStep';
|
import PermissionsStep from './features/PermissionsStep';
|
||||||
import WelcomeStep from './features/WelcomeStep';
|
import WelcomeStep from './features/WelcomeStep';
|
||||||
import {
|
import {
|
||||||
clearDesktopOnboardingStep,
|
clearDesktopOnboardingScreen,
|
||||||
getDesktopOnboardingStep,
|
getDesktopOnboardingScreen,
|
||||||
setDesktopOnboardingCompleted,
|
setDesktopOnboardingCompleted,
|
||||||
setDesktopOnboardingStep,
|
setDesktopOnboardingScreen,
|
||||||
} from './storage';
|
} from './storage';
|
||||||
|
import { DesktopOnboardingScreen, isDesktopOnboardingScreen } from './types';
|
||||||
|
|
||||||
const DesktopOnboardingPage = memo(() => {
|
const DesktopOnboardingPage = memo(() => {
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const [isMac, setIsMac] = useState(true);
|
const [isMac, setIsMac] = useState(true);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
// 从 localStorage 或 URL query 参数获取初始步骤
|
const flow = isMac
|
||||||
// 优先使用 localStorage 以支持重启后恢复
|
? [
|
||||||
const getInitialStep = useCallback(() => {
|
DesktopOnboardingScreen.Welcome,
|
||||||
// First try localStorage (for app restart scenario)
|
DesktopOnboardingScreen.Permissions,
|
||||||
const savedStep = getDesktopOnboardingStep();
|
DesktopOnboardingScreen.DataMode,
|
||||||
if (savedStep !== null) {
|
DesktopOnboardingScreen.Login,
|
||||||
return savedStep;
|
]
|
||||||
}
|
: [
|
||||||
// Then try URL params
|
DesktopOnboardingScreen.Welcome,
|
||||||
const stepParam = searchParams.get('step');
|
DesktopOnboardingScreen.DataMode,
|
||||||
if (stepParam) {
|
DesktopOnboardingScreen.Login,
|
||||||
const step = parseInt(stepParam, 10);
|
];
|
||||||
if (step >= 1 && step <= 4) return step;
|
|
||||||
}
|
const resolveScreenForPlatform = useCallback(
|
||||||
return 1;
|
(screen: DesktopOnboardingScreen) => {
|
||||||
|
if (!isMac && screen === DesktopOnboardingScreen.Permissions)
|
||||||
|
return DesktopOnboardingScreen.DataMode;
|
||||||
|
return screen;
|
||||||
|
},
|
||||||
|
[isMac],
|
||||||
|
);
|
||||||
|
|
||||||
|
const getRequestedScreenFromUrl = useCallback((): DesktopOnboardingScreen | null => {
|
||||||
|
const screenParam = searchParams.get('screen');
|
||||||
|
if (isDesktopOnboardingScreen(screenParam)) return screenParam;
|
||||||
|
|
||||||
|
return null;
|
||||||
}, [searchParams]);
|
}, [searchParams]);
|
||||||
|
|
||||||
const [currentStep, setCurrentStep] = useState(getInitialStep);
|
const [currentScreen, setCurrentScreen] = useState<DesktopOnboardingScreen>(
|
||||||
|
DesktopOnboardingScreen.Welcome,
|
||||||
|
);
|
||||||
|
|
||||||
// 持久化当前步骤到 localStorage
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setDesktopOnboardingStep(currentStep);
|
if (isLoading) return;
|
||||||
}, [currentStep]);
|
|
||||||
|
const saved = getDesktopOnboardingScreen();
|
||||||
|
const requested = getRequestedScreenFromUrl();
|
||||||
|
|
||||||
|
const initial = resolveScreenForPlatform(requested ?? saved ?? DesktopOnboardingScreen.Welcome);
|
||||||
|
|
||||||
|
setCurrentScreen(initial);
|
||||||
|
|
||||||
|
// Canonicalize URL to `?screen=...`
|
||||||
|
const currentUrlScreen = searchParams.get('screen');
|
||||||
|
if (currentUrlScreen !== initial) {
|
||||||
|
setSearchParams({ screen: initial });
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
getRequestedScreenFromUrl,
|
||||||
|
isLoading,
|
||||||
|
resolveScreenForPlatform,
|
||||||
|
searchParams,
|
||||||
|
setSearchParams,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Persist current screen to localStorage.
|
||||||
|
useEffect(() => {
|
||||||
|
if (isLoading) return;
|
||||||
|
setDesktopOnboardingScreen(currentScreen);
|
||||||
|
}, [currentScreen, isLoading]);
|
||||||
|
|
||||||
// 设置窗口大小和可调整性
|
// 设置窗口大小和可调整性
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -91,79 +130,48 @@ const DesktopOnboardingPage = memo(() => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 监听 URL query 参数变化
|
// Listen URL changes: allow deep-linking between screens.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const stepParam = searchParams.get('step');
|
if (isLoading) return;
|
||||||
if (stepParam) {
|
const requested = getRequestedScreenFromUrl();
|
||||||
const step = parseInt(stepParam, 10);
|
if (!requested) return;
|
||||||
if (step >= 1 && step <= 4 && step !== currentStep) {
|
const resolved = resolveScreenForPlatform(requested);
|
||||||
setCurrentStep(step);
|
if (resolved !== currentScreen) setCurrentScreen(resolved);
|
||||||
}
|
}, [currentScreen, getRequestedScreenFromUrl, isLoading, resolveScreenForPlatform]);
|
||||||
}
|
|
||||||
}, [searchParams, currentStep]);
|
|
||||||
|
|
||||||
const goToNextStep = useCallback(() => {
|
const goToNextStep = useCallback(() => {
|
||||||
setCurrentStep((prev) => {
|
setCurrentScreen((prev) => {
|
||||||
let nextStep: number;
|
const idx = flow.indexOf(prev);
|
||||||
// 如果是第1步(WelcomeStep),下一步根据平台决定
|
const next = flow[idx + 1];
|
||||||
switch (prev) {
|
|
||||||
case 1: {
|
|
||||||
nextStep = isMac ? 2 : 3; // macOS 显示权限页,其他平台跳过
|
|
||||||
|
|
||||||
break;
|
if (!next) {
|
||||||
}
|
// Complete onboarding.
|
||||||
case 2: {
|
setDesktopOnboardingCompleted();
|
||||||
// 如果是第2步(PermissionsStep,仅 macOS),下一步是第3步
|
clearDesktopOnboardingScreen();
|
||||||
nextStep = 3;
|
|
||||||
|
|
||||||
break;
|
// Restore window minimum size before hard reload (cleanup won't run due to hard navigation)
|
||||||
}
|
electronSystemService
|
||||||
case 3: {
|
.setWindowMinimumSize(APP_WINDOW_MIN_SIZE)
|
||||||
// 如果是第3步(DataModeStep),下一步是第4步
|
.catch(console.error)
|
||||||
nextStep = 4;
|
.finally(() => {
|
||||||
|
// Use hard reload instead of SPA navigation to ensure the app boots with the new desktop state.
|
||||||
|
window.location.replace('/');
|
||||||
|
});
|
||||||
|
|
||||||
break;
|
return prev;
|
||||||
}
|
|
||||||
case 4: {
|
|
||||||
// 如果是第4步(LoginStep),完成 onboarding
|
|
||||||
setDesktopOnboardingCompleted();
|
|
||||||
clearDesktopOnboardingStep(); // Clear persisted step since onboarding is complete
|
|
||||||
// Restore window minimum size before hard reload (cleanup won't run due to hard navigation)
|
|
||||||
electronSystemService
|
|
||||||
.setWindowMinimumSize(APP_WINDOW_MIN_SIZE)
|
|
||||||
.catch(console.error)
|
|
||||||
.finally(() => {
|
|
||||||
// Use hard reload instead of SPA navigation to ensure the app boots with the new desktop state.
|
|
||||||
window.location.replace('/');
|
|
||||||
});
|
|
||||||
return prev;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
nextStep = prev + 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// 更新 URL query 参数
|
|
||||||
setSearchParams({ step: nextStep.toString() });
|
setSearchParams({ screen: next });
|
||||||
return nextStep;
|
return next;
|
||||||
});
|
});
|
||||||
}, [isMac, setSearchParams]);
|
}, [isMac, setSearchParams]);
|
||||||
|
|
||||||
const goToPreviousStep = useCallback(() => {
|
const goToPreviousStep = useCallback(() => {
|
||||||
setCurrentStep((prev) => {
|
setCurrentScreen((prev) => {
|
||||||
if (prev <= 1) return 1;
|
const idx = flow.indexOf(prev);
|
||||||
let prevStep: number;
|
const prevScreen = flow[Math.max(0, idx - 1)] ?? DesktopOnboardingScreen.Welcome;
|
||||||
// 如果当前是第3步(DataModeStep),上一步根据平台决定
|
setSearchParams({ screen: prevScreen });
|
||||||
if (prev === 3) {
|
return prevScreen;
|
||||||
prevStep = isMac ? 2 : 1;
|
|
||||||
} else if (prev === 2) {
|
|
||||||
// 如果当前是第2步(PermissionsStep),上一步是第1步
|
|
||||||
prevStep = 1;
|
|
||||||
} else {
|
|
||||||
prevStep = prev - 1;
|
|
||||||
}
|
|
||||||
// 更新 URL query 参数
|
|
||||||
setSearchParams({ step: prevStep.toString() });
|
|
||||||
return prevStep;
|
|
||||||
});
|
});
|
||||||
}, [isMac, setSearchParams]);
|
}, [isMac, setSearchParams]);
|
||||||
|
|
||||||
@@ -172,21 +180,22 @@ const DesktopOnboardingPage = memo(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const renderStep = () => {
|
const renderStep = () => {
|
||||||
switch (currentStep) {
|
switch (currentScreen) {
|
||||||
case 1: {
|
case DesktopOnboardingScreen.Welcome: {
|
||||||
return <WelcomeStep onNext={goToNextStep} />;
|
return <WelcomeStep onNext={goToNextStep} />;
|
||||||
}
|
}
|
||||||
case 2: {
|
case DesktopOnboardingScreen.Permissions: {
|
||||||
// 仅 macOS 显示权限页
|
// macOS-only screen; fallback to DataMode if platform doesn't support.
|
||||||
if (!isMac) {
|
if (!isMac) {
|
||||||
return <DataModeStep onBack={goToPreviousStep} onNext={goToNextStep} />;
|
setCurrentScreen(DesktopOnboardingScreen.DataMode);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
return <PermissionsStep onBack={goToPreviousStep} onNext={goToNextStep} />;
|
return <PermissionsStep onBack={goToPreviousStep} onNext={goToNextStep} />;
|
||||||
}
|
}
|
||||||
case 3: {
|
case DesktopOnboardingScreen.DataMode: {
|
||||||
return <DataModeStep onBack={goToPreviousStep} onNext={goToNextStep} />;
|
return <DataModeStep onBack={goToPreviousStep} onNext={goToNextStep} />;
|
||||||
}
|
}
|
||||||
case 4: {
|
case DesktopOnboardingScreen.Login: {
|
||||||
return <LoginStep onBack={goToPreviousStep} onNext={goToNextStep} />;
|
return <LoginStep onBack={goToPreviousStep} onNext={goToNextStep} />;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { DesktopOnboardingScreen } from './types';
|
||||||
|
|
||||||
|
const DESKTOP_ONBOARDING_ROUTE = '/desktop-onboarding';
|
||||||
|
export const getDesktopOnboardingPath = (screen?: DesktopOnboardingScreen) => {
|
||||||
|
if (!screen) return DESKTOP_ONBOARDING_ROUTE;
|
||||||
|
return `${DESKTOP_ONBOARDING_ROUTE}?screen=${encodeURIComponent(screen)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const navigateToDesktopOnboarding = (screen?: DesktopOnboardingScreen) => {
|
||||||
|
location.href = getDesktopOnboardingPath(screen);
|
||||||
|
};
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import { DesktopOnboardingScreen, isDesktopOnboardingScreen } from './types';
|
||||||
|
|
||||||
export const DESKTOP_ONBOARDING_STORAGE_KEY = 'lobechat:desktop:onboarding:completed:v1';
|
export const DESKTOP_ONBOARDING_STORAGE_KEY = 'lobechat:desktop:onboarding:completed:v1';
|
||||||
export const DESKTOP_ONBOARDING_STEP_KEY = 'lobechat:desktop:onboarding:step:v1';
|
export const DESKTOP_ONBOARDING_SCREEN_KEY = 'lobechat:desktop:onboarding:screen:v1';
|
||||||
|
|
||||||
export const getDesktopOnboardingCompleted = () => {
|
export const getDesktopOnboardingCompleted = () => {
|
||||||
if (typeof window === 'undefined') return true;
|
if (typeof window === 'undefined') return true;
|
||||||
@@ -35,33 +37,29 @@ export const clearDesktopOnboardingCompleted = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the persisted onboarding step (for restoring after app restart)
|
* Get the persisted onboarding screen (for restoring after app restart)
|
||||||
*/
|
*/
|
||||||
export const getDesktopOnboardingStep = (): number | null => {
|
export const getDesktopOnboardingScreen = () => {
|
||||||
if (typeof window === 'undefined') return null;
|
if (typeof window === 'undefined') return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const step = window.localStorage.getItem(DESKTOP_ONBOARDING_STEP_KEY);
|
const screen = window.localStorage.getItem(DESKTOP_ONBOARDING_SCREEN_KEY);
|
||||||
if (step) {
|
if (!screen) return null;
|
||||||
const parsedStep = Number.parseInt(step, 10);
|
if (!isDesktopOnboardingScreen(screen)) return null;
|
||||||
if (parsedStep >= 1 && parsedStep <= 4) {
|
return screen;
|
||||||
return parsedStep;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Persist the current onboarding step
|
* Persist the current onboarding screen
|
||||||
*/
|
*/
|
||||||
export const setDesktopOnboardingStep = (step: number) => {
|
export const setDesktopOnboardingScreen = (screen: DesktopOnboardingScreen) => {
|
||||||
if (typeof window === 'undefined') return false;
|
if (typeof window === 'undefined') return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
window.localStorage.setItem(DESKTOP_ONBOARDING_STEP_KEY, step.toString());
|
window.localStorage.setItem(DESKTOP_ONBOARDING_SCREEN_KEY, screen);
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
@@ -69,13 +67,13 @@ export const setDesktopOnboardingStep = (step: number) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear the persisted onboarding step (called when onboarding completes)
|
* Clear the persisted onboarding screen (called when onboarding completes)
|
||||||
*/
|
*/
|
||||||
export const clearDesktopOnboardingStep = () => {
|
export const clearDesktopOnboardingScreen = () => {
|
||||||
if (typeof window === 'undefined') return false;
|
if (typeof window === 'undefined') return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
window.localStorage.removeItem(DESKTOP_ONBOARDING_STEP_KEY);
|
window.localStorage.removeItem(DESKTOP_ONBOARDING_SCREEN_KEY);
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export enum DesktopOnboardingScreen {
|
||||||
|
DataMode = 'data-mode',
|
||||||
|
Login = 'login',
|
||||||
|
Permissions = 'permissions',
|
||||||
|
Welcome = 'welcome',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isDesktopOnboardingScreen = (value: unknown): value is DesktopOnboardingScreen => {
|
||||||
|
if (typeof value !== 'string') return false;
|
||||||
|
return (Object.values(DesktopOnboardingScreen) as string[]).includes(value);
|
||||||
|
};
|
||||||
@@ -1,15 +1,17 @@
|
|||||||
import { ENABLE_BUSINESS_FEATURES } from '@lobechat/business-const';
|
import { ENABLE_BUSINESS_FEATURES } from '@lobechat/business-const';
|
||||||
import { Flexbox } from '@lobehub/ui';
|
import { Flexbox } from '@lobehub/ui';
|
||||||
import { useRouter } from '@/libs/next/navigation';
|
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { navigateToDesktopOnboarding } from '@/app/[variants]/(desktop)/desktop-onboarding/navigation';
|
||||||
import { clearDesktopOnboardingCompleted } from '@/app/[variants]/(desktop)/desktop-onboarding/storage';
|
import { clearDesktopOnboardingCompleted } from '@/app/[variants]/(desktop)/desktop-onboarding/storage';
|
||||||
|
import { DesktopOnboardingScreen } from '@/app/[variants]/(desktop)/desktop-onboarding/types';
|
||||||
import BusinessPanelContent from '@/business/client/features/User/BusinessPanelContent';
|
import BusinessPanelContent from '@/business/client/features/User/BusinessPanelContent';
|
||||||
import BrandWatermark from '@/components/BrandWatermark';
|
import BrandWatermark from '@/components/BrandWatermark';
|
||||||
import Menu from '@/components/Menu';
|
import Menu from '@/components/Menu';
|
||||||
import { isDesktop } from '@/const/version';
|
import { isDesktop } from '@/const/version';
|
||||||
import { enableBetterAuth, enableNextAuth } from '@/envs/auth';
|
import { enableBetterAuth, enableNextAuth } from '@/envs/auth';
|
||||||
|
import { useRouter } from '@/libs/next/navigation';
|
||||||
import { useUserStore } from '@/store/user';
|
import { useUserStore } from '@/store/user';
|
||||||
import { authSelectors } from '@/store/user/selectors';
|
import { authSelectors } from '@/store/user/selectors';
|
||||||
|
|
||||||
@@ -21,7 +23,7 @@ import { useMenu } from './useMenu';
|
|||||||
|
|
||||||
const PanelContent = memo<{ closePopover: () => void }>(({ closePopover }) => {
|
const PanelContent = memo<{ closePopover: () => void }>(({ closePopover }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const navigate = useNavigate();
|
|
||||||
const isLoginWithAuth = useUserStore(authSelectors.isLoginWithAuth);
|
const isLoginWithAuth = useUserStore(authSelectors.isLoginWithAuth);
|
||||||
const [openSignIn, signOut] = useUserStore((s) => [s.openLogin, s.logout]);
|
const [openSignIn, signOut] = useUserStore((s) => [s.openLogin, s.logout]);
|
||||||
const { mainItems, logoutItems } = useMenu();
|
const { mainItems, logoutItems } = useMenu();
|
||||||
@@ -35,17 +37,16 @@ const PanelContent = memo<{ closePopover: () => void }>(({ closePopover }) => {
|
|||||||
if (isDesktop) {
|
if (isDesktop) {
|
||||||
closePopover();
|
closePopover();
|
||||||
|
|
||||||
// Desktop: clear OIDC tokens (electron main) + re-enter desktop onboarding at Screen5.
|
|
||||||
try {
|
try {
|
||||||
const { remoteServerService } = await import('@/services/electron/remoteServer');
|
const { remoteServerService } = await import('@/services/electron/remoteServer');
|
||||||
await remoteServerService.clearRemoteServerConfig();
|
await remoteServerService.clearRemoteServerConfig();
|
||||||
} catch {
|
} catch (error) {
|
||||||
// Ignore: even if IPC is unavailable, still proceed to onboarding.
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
clearDesktopOnboardingCompleted();
|
||||||
|
signOut();
|
||||||
|
navigateToDesktopOnboarding(DesktopOnboardingScreen.Login);
|
||||||
}
|
}
|
||||||
|
|
||||||
clearDesktopOnboardingCompleted();
|
|
||||||
signOut();
|
|
||||||
navigate('/desktop-onboarding#5', { replace: true });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user