fix: drop the empty security step when no sign in plugin offers setup

This commit is contained in:
LukeGus committed 2026-11-03 21:27:29 -06:00
1 parent 80612c304b
commit 31df4b12e3
3 files changed
+38 -2

No files matched your search

+1
View File
@@ -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
+6 -2
View File
@@ -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]);
@@ -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: () => <p>enroll body</p>,
});
const offStep = onboardingSteps.register({
id: "x:factor",
pluginId: "x",
version: 1,
titleKey: "factor",
Component: () => <Plain label="factor" />,
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();
});
});