From d90e5e49abd12f20410a938aa916afa32a3fff84 Mon Sep 17 00:00:00 2001 From: Steve Sewell Date: Sat, 10 Oct 2026 15:36:12 -0700 Subject: [PATCH 1/3] Show first-run role while onboarding loads --- .../onboarding/FirstRunOnboarding.spec.tsx | 32 +++++++++++++++++-- .../src/app/onboarding/FirstRunOnboarding.tsx | 17 ++++++---- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/packages/toolkit/src/app/onboarding/FirstRunOnboarding.spec.tsx b/packages/toolkit/src/app/onboarding/FirstRunOnboarding.spec.tsx index 09b3f51c039..333ac2ad288 100644 --- a/packages/toolkit/src/app/onboarding/FirstRunOnboarding.spec.tsx +++ b/packages/toolkit/src/app/onboarding/FirstRunOnboarding.spec.tsx @@ -24,13 +24,17 @@ import { const toolkitI18nCatalog = createToolkitI18nCatalog({ messages: {} }); -function FirstRunOnboarding() { +function FirstRunOnboarding({ + initialFirstRun = false, +}: { + initialFirstRun?: boolean; +} = {}) { return ( - + ); } @@ -258,6 +262,30 @@ describe("FirstRunOnboarding", () => { expect(document.body.querySelector("[data-onboarding-screen]")).toBeNull(); }); + it("shows the first-run role step while the onboarding summary is loading", () => { + mocks.useOnboarding.mockReturnValue({ + firstRun: true, + loading: true, + error: null, + profile: null, + completeFirstRun: mocks.completeFirstRun, + completeFirstRunError: null, + }); + + act(() => { + root.render( + + + , + ); + }); + + expect( + document.body.querySelector('[data-onboarding-screen="role"]'), + ).not.toBeNull(); + expect(document.body.querySelector("[data-onboarding-loading]")).toBeNull(); + }); + it("does not show a close button during first-run setup", async () => { await act(async () => { root.render( diff --git a/packages/toolkit/src/app/onboarding/FirstRunOnboarding.tsx b/packages/toolkit/src/app/onboarding/FirstRunOnboarding.tsx index 214f3cf05e9..f12d4c236ef 100644 --- a/packages/toolkit/src/app/onboarding/FirstRunOnboarding.tsx +++ b/packages/toolkit/src/app/onboarding/FirstRunOnboarding.tsx @@ -554,15 +554,12 @@ export function FirstRunOnboarding({ ); } - if (loading || !profile) { + const canRenderRoleWhileLoading = + initialFirstRun && loading && screen === "role"; + if ((loading || !profile) && !canRenderRoleWhileLoading) { return ; } - // Every shared service Builder.io powers, the same list Infrastructure - // shows, plus the app's own headline capabilities it covers. - const builderCapabilities = getBuilderIncludedCapabilities( - profile.capabilities, - ); const handleBuilder = (provisionAccount = canActivateBuilderFreeCredits) => { if (previewMode) { handleFinish(null); @@ -736,6 +733,14 @@ export function FirstRunOnboarding({ } if (screen === "choice") { + if (!profile) return ; + + // Every shared service Builder.io powers, the same list Infrastructure + // shows, plus the app's own headline capabilities it covers. + const builderCapabilities = getBuilderIncludedCapabilities( + profile.capabilities, + ); + return ( Date: Sat, 10 Oct 2026 15:47:04 -0700 Subject: [PATCH 2/3] Keep first-run role selection visible on summary errors --- .../onboarding/FirstRunOnboarding.spec.tsx | 49 +++++++++++++++++++ .../src/app/onboarding/FirstRunOnboarding.tsx | 9 ++-- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/packages/toolkit/src/app/onboarding/FirstRunOnboarding.spec.tsx b/packages/toolkit/src/app/onboarding/FirstRunOnboarding.spec.tsx index 333ac2ad288..ac6d847ea6a 100644 --- a/packages/toolkit/src/app/onboarding/FirstRunOnboarding.spec.tsx +++ b/packages/toolkit/src/app/onboarding/FirstRunOnboarding.spec.tsx @@ -286,6 +286,55 @@ describe("FirstRunOnboarding", () => { expect(document.body.querySelector("[data-onboarding-loading]")).toBeNull(); }); + it("keeps the role selection visible if the onboarding summary fails", () => { + mocks.useOnboarding.mockReturnValue({ + firstRun: true, + loading: true, + error: null, + profile: null, + completeFirstRun: mocks.completeFirstRun, + completeFirstRunError: null, + }); + + act(() => { + root.render( + + + , + ); + }); + + act(() => { + container + .querySelector('[data-testid="first-run-role-developer"] input') + ?.click(); + }); + + mocks.useOnboarding.mockReturnValue({ + firstRun: true, + loading: false, + error: new Error("summary unavailable"), + profile: null, + completeFirstRun: mocks.completeFirstRun, + completeFirstRunError: null, + }); + + act(() => { + root.render( + + + , + ); + }); + + expect( + container.querySelector('[data-onboarding-screen="role"]'), + ).not.toBeNull(); + expect( + container.querySelector('[data-testid="first-run-role-developer"] input'), + ).toHaveProperty("checked", true); + }); + it("does not show a close button during first-run setup", async () => { await act(async () => { root.render( diff --git a/packages/toolkit/src/app/onboarding/FirstRunOnboarding.tsx b/packages/toolkit/src/app/onboarding/FirstRunOnboarding.tsx index f12d4c236ef..4a205b2e6ba 100644 --- a/packages/toolkit/src/app/onboarding/FirstRunOnboarding.tsx +++ b/packages/toolkit/src/app/onboarding/FirstRunOnboarding.tsx @@ -528,7 +528,10 @@ export function FirstRunOnboarding({ if (!firstRun) return null; - if (error) { + const canRenderRoleWithoutProfile = + initialFirstRun && screen === "role" && (loading || error != null); + + if (error && !canRenderRoleWithoutProfile) { return ( ; } From 62334a530efad927089f174c8c60dcdcbcb2a844 Mon Sep 17 00:00:00 2001 From: Steve Sewell Date: Sat, 10 Oct 2026 15:58:06 -0700 Subject: [PATCH 3/3] Track onboarding while summary loads --- .../onboarding/FirstRunOnboarding.spec.tsx | 54 +++++++++++++++++++ .../src/app/onboarding/FirstRunOnboarding.tsx | 28 ++++++---- 2 files changed, 71 insertions(+), 11 deletions(-) diff --git a/packages/toolkit/src/app/onboarding/FirstRunOnboarding.spec.tsx b/packages/toolkit/src/app/onboarding/FirstRunOnboarding.spec.tsx index ac6d847ea6a..a42d8d313c0 100644 --- a/packages/toolkit/src/app/onboarding/FirstRunOnboarding.spec.tsx +++ b/packages/toolkit/src/app/onboarding/FirstRunOnboarding.spec.tsx @@ -284,6 +284,60 @@ describe("FirstRunOnboarding", () => { document.body.querySelector('[data-onboarding-screen="role"]'), ).not.toBeNull(); expect(document.body.querySelector("[data-onboarding-loading]")).toBeNull(); + expect(mocks.trackOnboardingEvent).toHaveBeenCalledWith( + "onboarding_started", + { flow: "first_run" }, + ); + expect(mocks.trackOnboardingEvent).toHaveBeenCalledWith( + "onboarding_step_viewed", + expect.objectContaining({ flow: "first_run", step_id: "role" }), + ); + }); + + it("does not emit a second started event when the summary resolves", () => { + mocks.useOnboarding.mockReturnValue({ + firstRun: true, + loading: true, + error: null, + profile: null, + completeFirstRun: mocks.completeFirstRun, + completeFirstRunError: null, + }); + + act(() => { + root.render( + + + , + ); + }); + + mocks.useOnboarding.mockReturnValue({ + firstRun: true, + loading: false, + error: null, + profile: { + appId: "builder-app", + appName: "Builder App", + capabilities: [], + }, + completeFirstRun: mocks.completeFirstRun, + completeFirstRunError: null, + }); + + act(() => { + root.render( + + + , + ); + }); + + expect( + mocks.trackOnboardingEvent.mock.calls.filter( + ([name]) => name === "onboarding_started", + ), + ).toHaveLength(1); }); it("keeps the role selection visible if the onboarding summary fails", () => { diff --git a/packages/toolkit/src/app/onboarding/FirstRunOnboarding.tsx b/packages/toolkit/src/app/onboarding/FirstRunOnboarding.tsx index 4a205b2e6ba..077c552c5b0 100644 --- a/packages/toolkit/src/app/onboarding/FirstRunOnboarding.tsx +++ b/packages/toolkit/src/app/onboarding/FirstRunOnboarding.tsx @@ -183,6 +183,10 @@ export function FirstRunOnboarding({ const [screen, setScreen] = useState(() => previewStep === "references" ? "extension" : (previewStep ?? "role"), ); + const canRenderRoleWithoutProfile = + initialFirstRun && screen === "role" && (loading || error != null); + const canTrackOnboardingScreen = + (!loading && profile !== null) || canRenderRoleWithoutProfile; const [extensionPlacement, setExtensionPlacement] = useState("after-setup"); const [extensionIndex, setExtensionIndex] = useState(0); @@ -308,6 +312,7 @@ export function FirstRunOnboarding({ const setupSkipStartedRef = useRef(false); const onboardingTerminalRef = useRef(false); const abandonmentTrackedRef = useRef(false); + const onboardingStartedRef = useRef(false); const setupAttemptRef = useRef(null); const builderSetupAttemptRef = useRef(null); const stepViewRef = useRef<{ key: string; id: string } | null>(null); @@ -367,12 +372,18 @@ export function FirstRunOnboarding({ [completeFirstRun, extensionIndex, navigate, trackFirstRunStepCompleted], ); useEffect(() => { - if (!previewMode && firstRun && !loading && profile) { + if ( + !previewMode && + firstRun && + canTrackOnboardingScreen && + !onboardingStartedRef.current + ) { + onboardingStartedRef.current = true; trackOnboardingEvent("onboarding_started", { flow: "first_run" }); } - }, [firstRun, loading, previewMode, profile]); + }, [canTrackOnboardingScreen, firstRun, previewMode]); useEffect(() => { - if (previewMode || !firstRun || loading || !profile) { + if (previewMode || !firstRun || !canTrackOnboardingScreen) { stepViewRef.current = null; return; } @@ -399,13 +410,12 @@ export function FirstRunOnboarding({ extensionPlacement, extensionStepIndex, firstRun, - loading, previewMode, - profile, + canTrackOnboardingScreen, screen, ]); useEffect(() => { - if (previewMode || !firstRun || loading || !profile) return; + if (previewMode || !firstRun || !canTrackOnboardingScreen) return; const handlePageHide = (event: PageTransitionEvent) => { if (event.persisted) return; if ( @@ -436,9 +446,8 @@ export function FirstRunOnboarding({ extensionPlacement, extensionStepIndex, firstRun, - loading, previewMode, - profile, + canTrackOnboardingScreen, screen, ]); const beginBeforeSetup = useCallback(() => { @@ -528,9 +537,6 @@ export function FirstRunOnboarding({ if (!firstRun) return null; - const canRenderRoleWithoutProfile = - initialFirstRun && screen === "role" && (loading || error != null); - if (error && !canRenderRoleWithoutProfile) { return (