fix(FN-2704): gate auth onboarding behind setup wizard
- Prevent useAuthOnboarding from opening model onboarding while the setup wizard is active - Add app-level wiring so onboarding flow respects setup wizard visibility state - Add regression tests covering setup wizard gating and non-stacking onboarding behavior
This commit is contained in:
@@ -393,6 +393,7 @@ function AppInner() {
|
|||||||
// Auth and onboarding bootstrap logic extracted to a dedicated hook.
|
// Auth and onboarding bootstrap logic extracted to a dedicated hook.
|
||||||
useAuthOnboarding({
|
useAuthOnboarding({
|
||||||
projectId: currentProject?.id,
|
projectId: currentProject?.id,
|
||||||
|
setupWizardOpen: modalManager.setupWizardOpen,
|
||||||
openModelOnboarding: modalManager.openModelOnboarding,
|
openModelOnboarding: modalManager.openModelOnboarding,
|
||||||
openSettings: modalManager.openSettings,
|
openSettings: modalManager.openSettings,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ describe("useAuthOnboarding", () => {
|
|||||||
renderHook(() =>
|
renderHook(() =>
|
||||||
useAuthOnboarding({
|
useAuthOnboarding({
|
||||||
projectId: "proj_123",
|
projectId: "proj_123",
|
||||||
|
setupWizardOpen: false,
|
||||||
openModelOnboarding,
|
openModelOnboarding,
|
||||||
openSettings,
|
openSettings,
|
||||||
}),
|
}),
|
||||||
@@ -76,6 +77,7 @@ describe("useAuthOnboarding", () => {
|
|||||||
renderHook(() =>
|
renderHook(() =>
|
||||||
useAuthOnboarding({
|
useAuthOnboarding({
|
||||||
projectId: "proj_123",
|
projectId: "proj_123",
|
||||||
|
setupWizardOpen: false,
|
||||||
openModelOnboarding,
|
openModelOnboarding,
|
||||||
openSettings,
|
openSettings,
|
||||||
}),
|
}),
|
||||||
@@ -101,6 +103,7 @@ describe("useAuthOnboarding", () => {
|
|||||||
renderHook(() =>
|
renderHook(() =>
|
||||||
useAuthOnboarding({
|
useAuthOnboarding({
|
||||||
projectId: "proj_123",
|
projectId: "proj_123",
|
||||||
|
setupWizardOpen: false,
|
||||||
openModelOnboarding,
|
openModelOnboarding,
|
||||||
openSettings,
|
openSettings,
|
||||||
}),
|
}),
|
||||||
@@ -127,6 +130,7 @@ describe("useAuthOnboarding", () => {
|
|||||||
renderHook(() =>
|
renderHook(() =>
|
||||||
useAuthOnboarding({
|
useAuthOnboarding({
|
||||||
projectId: "proj_123",
|
projectId: "proj_123",
|
||||||
|
setupWizardOpen: false,
|
||||||
openModelOnboarding,
|
openModelOnboarding,
|
||||||
openSettings,
|
openSettings,
|
||||||
}),
|
}),
|
||||||
@@ -152,6 +156,7 @@ describe("useAuthOnboarding", () => {
|
|||||||
renderHook(() =>
|
renderHook(() =>
|
||||||
useAuthOnboarding({
|
useAuthOnboarding({
|
||||||
projectId: "proj_123",
|
projectId: "proj_123",
|
||||||
|
setupWizardOpen: false,
|
||||||
openModelOnboarding,
|
openModelOnboarding,
|
||||||
openSettings,
|
openSettings,
|
||||||
}),
|
}),
|
||||||
@@ -173,6 +178,7 @@ describe("useAuthOnboarding", () => {
|
|||||||
renderHook(() =>
|
renderHook(() =>
|
||||||
useAuthOnboarding({
|
useAuthOnboarding({
|
||||||
projectId: "proj_123",
|
projectId: "proj_123",
|
||||||
|
setupWizardOpen: false,
|
||||||
openModelOnboarding,
|
openModelOnboarding,
|
||||||
openSettings,
|
openSettings,
|
||||||
}),
|
}),
|
||||||
@@ -187,6 +193,66 @@ describe("useAuthOnboarding", () => {
|
|||||||
expect(mockFetchGlobalSettings).not.toHaveBeenCalled();
|
expect(mockFetchGlobalSettings).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not auto-trigger model onboarding when setupWizardOpen is true", async () => {
|
||||||
|
mockFetchAuthStatus.mockResolvedValue({
|
||||||
|
providers: [{ id: "openai", name: "OpenAI", authenticated: false }],
|
||||||
|
});
|
||||||
|
mockFetchGlobalSettings.mockResolvedValue({
|
||||||
|
modelOnboardingComplete: false,
|
||||||
|
defaultProvider: undefined,
|
||||||
|
defaultModelId: undefined,
|
||||||
|
} as never);
|
||||||
|
|
||||||
|
renderHook(() =>
|
||||||
|
useAuthOnboarding({
|
||||||
|
projectId: "proj_123",
|
||||||
|
setupWizardOpen: true,
|
||||||
|
openModelOnboarding,
|
||||||
|
openSettings,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(openModelOnboarding).not.toHaveBeenCalled();
|
||||||
|
expect(mockFetchAuthStatus).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("auto-triggers model onboarding when setupWizardOpen transitions from true to false", async () => {
|
||||||
|
mockFetchAuthStatus.mockResolvedValue({
|
||||||
|
providers: [{ id: "openai", name: "OpenAI", authenticated: false }],
|
||||||
|
});
|
||||||
|
mockFetchGlobalSettings.mockResolvedValue({
|
||||||
|
modelOnboardingComplete: false,
|
||||||
|
defaultProvider: undefined,
|
||||||
|
defaultModelId: undefined,
|
||||||
|
} as never);
|
||||||
|
|
||||||
|
const { rerender } = renderHook(
|
||||||
|
({ setupWizardOpen }: { setupWizardOpen: boolean }) =>
|
||||||
|
useAuthOnboarding({
|
||||||
|
projectId: "proj_123",
|
||||||
|
setupWizardOpen,
|
||||||
|
openModelOnboarding,
|
||||||
|
openSettings,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
initialProps: { setupWizardOpen: true },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(openModelOnboarding).not.toHaveBeenCalled();
|
||||||
|
expect(mockFetchAuthStatus).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
rerender({ setupWizardOpen: false });
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(openModelOnboarding).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// --- One-shot guard ---
|
// --- One-shot guard ---
|
||||||
|
|
||||||
it("does not re-trigger onboarding when projectId changes after initial bootstrap", async () => {
|
it("does not re-trigger onboarding when projectId changes after initial bootstrap", async () => {
|
||||||
@@ -204,6 +270,7 @@ describe("useAuthOnboarding", () => {
|
|||||||
({ projectId }: { projectId: string }) =>
|
({ projectId }: { projectId: string }) =>
|
||||||
useAuthOnboarding({
|
useAuthOnboarding({
|
||||||
projectId,
|
projectId,
|
||||||
|
setupWizardOpen: false,
|
||||||
openModelOnboarding,
|
openModelOnboarding,
|
||||||
openSettings,
|
openSettings,
|
||||||
}),
|
}),
|
||||||
@@ -238,6 +305,7 @@ describe("useAuthOnboarding", () => {
|
|||||||
({ open }: { open: () => void }) =>
|
({ open }: { open: () => void }) =>
|
||||||
useAuthOnboarding({
|
useAuthOnboarding({
|
||||||
projectId: "proj_123",
|
projectId: "proj_123",
|
||||||
|
setupWizardOpen: false,
|
||||||
openModelOnboarding: open,
|
openModelOnboarding: open,
|
||||||
openSettings,
|
openSettings,
|
||||||
}),
|
}),
|
||||||
@@ -273,6 +341,7 @@ describe("useAuthOnboarding", () => {
|
|||||||
({ open }: { open: (section?: string) => void }) =>
|
({ open }: { open: (section?: string) => void }) =>
|
||||||
useAuthOnboarding({
|
useAuthOnboarding({
|
||||||
projectId: "proj_123",
|
projectId: "proj_123",
|
||||||
|
setupWizardOpen: false,
|
||||||
openModelOnboarding,
|
openModelOnboarding,
|
||||||
openSettings: open,
|
openSettings: open,
|
||||||
}),
|
}),
|
||||||
@@ -311,6 +380,7 @@ describe("useAuthOnboarding", () => {
|
|||||||
renderHook(() =>
|
renderHook(() =>
|
||||||
useAuthOnboarding({
|
useAuthOnboarding({
|
||||||
projectId: "proj_123",
|
projectId: "proj_123",
|
||||||
|
setupWizardOpen: false,
|
||||||
openModelOnboarding,
|
openModelOnboarding,
|
||||||
openSettings,
|
openSettings,
|
||||||
}),
|
}),
|
||||||
@@ -341,6 +411,7 @@ describe("useAuthOnboarding", () => {
|
|||||||
renderHook(() =>
|
renderHook(() =>
|
||||||
useAuthOnboarding({
|
useAuthOnboarding({
|
||||||
projectId: "proj_123",
|
projectId: "proj_123",
|
||||||
|
setupWizardOpen: false,
|
||||||
openModelOnboarding,
|
openModelOnboarding,
|
||||||
openSettings,
|
openSettings,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type { SectionId } from "../components/SettingsModal";
|
|||||||
|
|
||||||
export interface UseAuthOnboardingOptions {
|
export interface UseAuthOnboardingOptions {
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
|
setupWizardOpen: boolean;
|
||||||
openModelOnboarding: () => void;
|
openModelOnboarding: () => void;
|
||||||
openSettings: (section?: SectionId) => void;
|
openSettings: (section?: SectionId) => void;
|
||||||
}
|
}
|
||||||
@@ -24,6 +25,7 @@ export interface UseAuthOnboardingOptions {
|
|||||||
*/
|
*/
|
||||||
export function useAuthOnboarding({
|
export function useAuthOnboarding({
|
||||||
projectId,
|
projectId,
|
||||||
|
setupWizardOpen,
|
||||||
openModelOnboarding,
|
openModelOnboarding,
|
||||||
openSettings,
|
openSettings,
|
||||||
}: UseAuthOnboardingOptions): void {
|
}: UseAuthOnboardingOptions): void {
|
||||||
@@ -32,6 +34,9 @@ export function useAuthOnboarding({
|
|||||||
const hasTriggeredRef = useRef(false);
|
const hasTriggeredRef = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Defer auto-triggering while setup wizard is open.
|
||||||
|
// Important: this must run before consuming the one-shot flag.
|
||||||
|
if (setupWizardOpen) return;
|
||||||
// Skip if we've already triggered (one-shot guard)
|
// Skip if we've already triggered (one-shot guard)
|
||||||
if (hasTriggeredRef.current) return;
|
if (hasTriggeredRef.current) return;
|
||||||
// Mark as triggered immediately to prevent any race condition on re-runs
|
// Mark as triggered immediately to prevent any race condition on re-runs
|
||||||
@@ -82,5 +87,5 @@ export function useAuthOnboarding({
|
|||||||
// Fail silently - non-blocking behavior preserves dashboard usability.
|
// Fail silently - non-blocking behavior preserves dashboard usability.
|
||||||
// Onboarding can be manually triggered later via Settings if needed.
|
// Onboarding can be manually triggered later via Settings if needed.
|
||||||
});
|
});
|
||||||
}, [projectId, openModelOnboarding, openSettings]);
|
}, [projectId, setupWizardOpen, openModelOnboarding, openSettings]);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user