diff --git a/desktop/src/features/onboarding/ui/CommunityOnboardingFlow.tsx b/desktop/src/features/onboarding/ui/CommunityOnboardingFlow.tsx index e09fd8404..25443ecd3 100644 --- a/desktop/src/features/onboarding/ui/CommunityOnboardingFlow.tsx +++ b/desktop/src/features/onboarding/ui/CommunityOnboardingFlow.tsx @@ -109,6 +109,26 @@ function AvatarCircle({ ); } +function LoadingDots({ label }: { label: string }) { + return ( + + {[0, 1, 2].map((index) => ( + + ); +} + export function CommunityOnboardingFlow({ onCancel, onConnect, @@ -126,6 +146,8 @@ export function CommunityOnboardingFlow({ [], ); const [isPending, setIsPending] = React.useState(false); + const [starterChannelFailureCount, setStarterChannelFailureCount] = + React.useState(0); const [deniedPubkey, setDeniedPubkey] = React.useState(""); const [isMembershipDenied, setIsMembershipDenied] = React.useState(false); const [isCommunityChangeOpen, setIsCommunityChangeOpen] = @@ -228,6 +250,7 @@ export function CommunityOnboardingFlow({ } await finish(); } catch (error) { + setStarterChannelFailureCount((count) => count + 1); update({ error: error instanceof Error ? error.message : String(error), }); @@ -235,6 +258,12 @@ export function CommunityOnboardingFlow({ } }, [finish, isPending, queryClient, relayUrl, update]); + const backToProfile = React.useCallback(() => { + if (isPending) return; + setStarterChannelFailureCount(0); + update({ stage: "profile", error: undefined }); + }, [isPending, update]); + const isProfileStage = transaction?.stage === "profile"; const isTeamStage = transaction?.stage === "team-intro" || @@ -340,9 +369,9 @@ export function CommunityOnboardingFlow({ className={cn( "relative w-full text-center", isProfileStage - ? "buzz-onboarding-step-frame flex max-w-[500px] flex-col justify-center" + ? "buzz-onboarding-step-frame flex max-w-[500px] flex-col items-center" : isTeamStage - ? "buzz-onboarding-step-frame flex max-w-[760px] flex-col justify-center" + ? "buzz-onboarding-step-frame flex max-w-[760px] flex-col items-center" : "flex min-h-dvh max-w-[560px] flex-col justify-center py-8", )} data-testid="community-onboarding-body" @@ -405,7 +434,7 @@ export function CommunityOnboardingFlow({ ) : ( <> -
+

Build your profile

Add a name and avatar. They’ll show up on your messages, @@ -468,66 +497,76 @@ export function CommunityOnboardingFlow({

Meet your starter team

Buzz lets you bring multiple agents into the same workspace. - This team will help you get started using Buzz. + Your team will help you get started using Buzz.

- {starterPersonas.length > 0 ? ( -
- {starterPersonas.map((persona) => { - const animationUrl = - STARTER_PERSONA_ANIMATIONS[persona.displayName]; - return ( -
- {animationUrl ? ( - {`${persona.displayName} - ) : ( - - )} - - {persona.displayName} - -
- ); - })} -
- ) : null} +
+ {starterPersonas.length > 0 ? ( +
+ {starterPersonas.map((persona) => { + const animationUrl = + STARTER_PERSONA_ANIMATIONS[persona.displayName]; + return ( +
+ {animationUrl ? ( + {`${persona.displayName} + ) : ( + + )} + + {persona.displayName} + +
+ ); + })} +
+ ) : null} +
{transaction.error ? ( -

+

{transaction.error} + {starterChannelFailureCount === 1 ? " Try again." : null}

) : null} + - {transaction.error ? ( - - ) : null} )} diff --git a/desktop/tests/e2e/deep-link-invite.spec.ts b/desktop/tests/e2e/deep-link-invite.spec.ts index efd3d9337..1ca8475cc 100644 --- a/desktop/tests/e2e/deep-link-invite.spec.ts +++ b/desktop/tests/e2e/deep-link-invite.spec.ts @@ -243,7 +243,7 @@ test("queued add-community links open and acknowledge one at a time", async ({ ]); }); -test("Welcome failure can be skipped without abandoning community onboarding", async ({ +test("Welcome failure retries once before allowing starter channel setup to be skipped", async ({ page, }) => { const welcomeError = "Channel creation is not permitted."; @@ -277,7 +277,7 @@ test("Welcome failure can be skipped without abandoning community onboarding", a ); await installMockBridge( page, - { ensureStarterChannelsErrors: [welcomeError, welcomeError] }, + { ensureStarterChannelsErrors: [welcomeError, welcomeError, welcomeError] }, { relayWsUrl: COMMUNITY_RELAY_URL, skipOnboardingSeed: true }, ); await page.goto("/"); @@ -291,30 +291,59 @@ test("Welcome failure can be skipped without abandoning community onboarding", a ); } - await page.getByRole("button", { name: "Enter hive" }).click(); + const enterButton = page.getByRole("button", { name: "Take me to Buzz" }); + await enterButton.click(); + + await expect(page.getByText(`${welcomeError} Try again.`)).toBeVisible(); + await expect(enterButton).toBeEnabled(); + const backButton = page.getByRole("button", { name: "Back" }); + await expect(backButton).toBeVisible(); + await backButton.click(); - await expect(page.getByText(welcomeError)).toBeVisible(); await expect( - page.getByRole("button", { name: "Preparing Welcome…" }), - ).toBeEnabled(); - const skip = page.getByRole("button", { name: "Skip for now" }); - await expect(skip).toBeVisible(); - await skip.click(); + page.getByRole("heading", { name: "Build your profile" }), + ).toBeVisible(); + await page.getByLabel("Community display name").fill("Tyler"); + await page.getByTestId("community-profile-next").click(); + + await enterButton.click(); + await expect(page.getByText(`${welcomeError} Try again.`)).toBeVisible(); + await expect(enterButton).toBeEnabled(); + + await enterButton.click(); + + const skipButton = page.getByRole("button", { name: "Skip for now" }); + await expect(page.getByText(welcomeError, { exact: true })).toBeVisible(); + await expect(skipButton).toBeEnabled(); + await expect(page.getByRole("button", { name: "Back" })).toBeVisible(); + + const starterChannelAttempts = await page.evaluate( + () => + window.__BUZZ_E2E_COMMANDS__?.filter( + (command) => command === "ensure_starter_channels", + ).length ?? 0, + ); + expect(starterChannelAttempts).toBe(3); + + await skipButton.click(); - const completionKey = `buzz-community-onboarding-complete.v1:${encodeURIComponent(COMMUNITY_RELAY_URL)}:${WELCOME_FAILURE_PUBKEY}`; + await expect(page.getByTestId("community-onboarding-flow")).toHaveCount(0); + expect( + await page.evaluate( + () => + window.__BUZZ_E2E_COMMANDS__?.filter( + (command) => command === "ensure_starter_channels", + ).length ?? 0, + ), + ).toBe(3); await expect .poll(() => page.evaluate( - ({ completion, transaction }) => ({ - completion: window.localStorage.getItem(completion), - transaction: window.localStorage.getItem(transaction), - }), - { completion: completionKey, transaction: TRANSACTION_STORAGE_KEY }, + (transaction) => window.localStorage.getItem(transaction), + TRANSACTION_STORAGE_KEY, ), ) - .toEqual({ completion: "true", transaction: null }); - await expect(page.getByTestId("community-onboarding-flow")).toHaveCount(0); - await expect(page.getByTestId("app-sidebar")).toBeVisible(); + .toBeNull(); }); test("persisted deep-link invite hands off to Joining after machine onboarding", async ({ diff --git a/desktop/tests/e2e/onboarding.spec.ts b/desktop/tests/e2e/onboarding.spec.ts index 947e55c7a..28f1865c5 100644 --- a/desktop/tests/e2e/onboarding.spec.ts +++ b/desktop/tests/e2e/onboarding.spec.ts @@ -987,18 +987,15 @@ test("connected first-community profile step cannot discard resumable onboarding await expect( page.getByRole("heading", { name: "Build your profile" }), ).toBeVisible(); - const profileMain = page.getByTestId("community-profile-main"); - const profileMainBox = await profileMain.boundingBox(); - const viewport = page.viewportSize(); - if (!profileMainBox || !viewport) { - throw new Error("Could not measure community profile body position"); + const profileHeading = page.getByRole("heading", { + name: "Build your profile", + }); + await expect(profileHeading).toBeVisible(); + const profileHeadingBox = await profileHeading.boundingBox(); + if (!profileHeadingBox) { + throw new Error("Could not measure community profile heading position"); } - const chromeOffset = 106; - const footerOffset = 144; - const profileMainCenterY = profileMainBox.y + profileMainBox.height / 2; - const centeredInUsableLaneY = - chromeOffset + (viewport.height - chromeOffset - footerOffset) / 2; - expect(Math.abs(profileMainCenterY - centeredInUsableLaneY)).toBeLessThan(32); + expect(Math.abs(profileHeadingBox.y - 106)).toBeLessThan(8); const nameKey = page.getByTestId("community-profile-name-key"); const avatarButton = page.getByTestId("community-avatar-open"); await expect(nameKey).toBeVisible();