diff --git a/CHANGELOG.md b/CHANGELOG.md index ce38086e9..184a735c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - The first run plugin picker no longer sets the default layout and themes to Remove. They always stay on - Alerts with no icon no longer squeeze their text to one word per line - Password fields with the show button are the same height as the other fields next to them +- The Secure your account setup step no longer shows up empty when no passkey or authenticator plugin is on ## 26.11.0 diff --git a/src/ui/onboarding/OnboardingStage.tsx b/src/ui/onboarding/OnboardingStage.tsx index e80c9182d..bb0ffd40b 100644 --- a/src/ui/onboarding/OnboardingStage.tsx +++ b/src/ui/onboarding/OnboardingStage.tsx @@ -19,6 +19,7 @@ import { } from "./onboarding-registry"; import { computeFlow, + isStepRelevant, ONBOARDING_ADMIN_PERMISSION, seenFor, } from "./onboarding-flow"; @@ -109,10 +110,13 @@ export function OnboardingStage({ const live = computeFlow(candidates, seenAtStart.current, ctx); for (const step of live) sticky.current.add(step.id); // A step that was in the run stays in it (the picker stops being - // pending once applied), unless its plugin went away. + // pending once applied), unless its plugin went away or it no longer + // applies, like Security once no sign in plugin offers anything. const kept = allSteps.filter( (step) => - sticky.current.has(step.id) && !live.some((s) => s.id === step.id), + sticky.current.has(step.id) && + !live.some((s) => s.id === step.id) && + (!!step.forcePending || isStepRelevant(step, ctx)), ); return sortSteps([...live, ...kept]); }, [allSteps, ctx]); diff --git a/src/ui/tests/onboarding/OnboardingStage.test.tsx b/src/ui/tests/onboarding/OnboardingStage.test.tsx index 977947bda..feea5286d 100644 --- a/src/ui/tests/onboarding/OnboardingStage.test.tsx +++ b/src/ui/tests/onboarding/OnboardingStage.test.tsx @@ -80,6 +80,7 @@ import { OnboardingStage } from "@/onboarding/OnboardingStage"; import { UiPreferencesProvider } from "@/contexts/UiPreferencesContext"; import { setPermissionsForTesting } from "@/hooks/use-permissions"; import { onboardingSteps } from "@/onboarding/onboarding-registry"; +import { registerSecondFactor } from "@/plugin-host/auth-registry"; import { defaultUiPreferences, type UiPreferences, @@ -216,4 +217,34 @@ describe("OnboardingStage", () => { ); await waitFor(() => expect(onDone).toHaveBeenCalled()); }); + it("drops a step that stops applying after it joined the run", async () => { + const offFactor = registerSecondFactor({ + id: "totp", + pluginId: "totp", + titleKey: "totp", + component: () => null, + enrollment: () =>

enroll body

, + }); + const offStep = onboardingSteps.register({ + id: "x:factor", + pluginId: "x", + version: 1, + titleKey: "factor", + Component: () => , + audience: "all", + position: 50, + isRelevant: (ctx) => ctx.canEnrollSecondFactor === true, + }); + renderStage("full"); + await screen.findByText("preset body"); + act(() => offFactor()); + next(); + await screen.findByText("picker body"); + await act(async () => next()); + await screen.findByText("appearance body"); + next(); + await screen.findByText("security body"); + expect(screen.queryByText("factor body")).toBeNull(); + offStep(); + }); });