Align intro-to-your-team onboarding (#2102)

Signed-off-by: npub19x6jnl6rhepymwyl2xlltz3ce7rfg2ktllle3g2vu59n3s490k8s9n40l3 <29b529ff43be424db89f51bff58a38cf86942acbffff98a14ce50b38c2a57d8f@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub19x6jnl6rhepymwyl2xlltz3ce7rfg2ktllle3g2vu59n3s490k8s9n40l3 <29b529ff43be424db89f51bff58a38cf86942acbffff98a14ce50b38c2a57d8f@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
cynfria
2026-07-19 08:45:33 -07:00
committed by GitHub
parent c12257d57a
commit b6aa71789d
3 changed files with 146 additions and 81 deletions

View File

@@ -109,6 +109,26 @@ function AvatarCircle({
);
}
function LoadingDots({ label }: { label: string }) {
return (
<span
aria-label={label}
className="inline-flex items-center justify-center gap-1"
data-testid="community-team-intro-loading-dots"
role="status"
>
{[0, 1, 2].map((index) => (
<span
aria-hidden="true"
className="h-1.5 w-1.5 animate-bounce rounded-full bg-current motion-reduce:animate-none"
key={index}
style={{ animationDelay: `${index * 120}ms` }}
/>
))}
</span>
);
}
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({
</div>
) : (
<>
<div data-testid="community-profile-main">
<div className="w-full" data-testid="community-profile-main">
<h1 className="text-title font-normal">Build your profile</h1>
<p className="mx-auto mt-3 max-w-[380px] text-sm leading-6 text-foreground/80">
Add a name and avatar. They’ll show up on your messages,
@@ -468,66 +497,76 @@ export function CommunityOnboardingFlow({
<h1 className="text-title font-normal">Meet your starter team</h1>
<p className="mx-auto mt-3 max-w-[400px] text-sm leading-6 text-foreground/80">
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.
</p>
{starterPersonas.length > 0 ? (
<div className="mt-10 flex flex-wrap justify-center gap-8">
{starterPersonas.map((persona) => {
const animationUrl =
STARTER_PERSONA_ANIMATIONS[persona.displayName];
return (
<div
className="flex w-40 flex-col items-center gap-3"
key={persona.id}
>
{animationUrl ? (
<img
alt={`${persona.displayName} animated character`}
className="h-40 w-40 object-contain"
data-testid={`starter-persona-${persona.displayName.toLowerCase()}`}
src={animationUrl}
/>
) : (
<ProfileAvatar
avatarUrl={persona.avatarUrl}
className="h-28 w-28 text-3xl"
label={persona.displayName}
/>
)}
<span className="font-mono text-xs font-medium uppercase tracking-[0.15em]">
{persona.displayName}
</span>
</div>
);
})}
</div>
) : null}
<div className="flex w-full flex-1 items-center justify-center py-10">
{starterPersonas.length > 0 ? (
<div className="flex flex-wrap justify-center gap-8">
{starterPersonas.map((persona) => {
const animationUrl =
STARTER_PERSONA_ANIMATIONS[persona.displayName];
return (
<div
className="flex w-40 flex-col items-center gap-3"
key={persona.id}
>
{animationUrl ? (
<img
alt={`${persona.displayName} animated character`}
className="h-40 w-40 object-contain"
data-testid={`starter-persona-${persona.displayName.toLowerCase()}`}
src={animationUrl}
/>
) : (
<ProfileAvatar
avatarUrl={persona.avatarUrl}
className="h-28 w-28 text-3xl"
label={persona.displayName}
/>
)}
<span className="font-mono text-xs font-medium uppercase tracking-[0.15em]">
{persona.displayName}
</span>
</div>
);
})}
</div>
) : null}
</div>
{transaction.error ? (
<p className="mt-4 text-sm text-destructive">
<p className="text-sm text-destructive">
{transaction.error}
{starterChannelFailureCount === 1 ? " Try again." : null}
</p>
) : null}
<OnboardingFooter>
<Button
className={ONBOARDING_PRIMARY_CTA_CLASS}
data-testid="community-team-intro-enter"
disabled={isPending || transaction.stage === "entering"}
onClick={() => void finalize()}
onClick={() =>
void (starterChannelFailureCount >= 2
? finish()
: finalize())
}
>
{transaction.stage === "finalizing" ||
transaction.stage === "entering"
? "Preparing Welcome…"
: `Enter ${transaction.communityName}`}
{isPending || transaction.stage === "entering" ? (
<LoadingDots label="Preparing Welcome" />
) : starterChannelFailureCount >= 2 ? (
"Skip for now"
) : (
"Take me to Buzz"
)}
</Button>
<Button
className="h-9 rounded-full bg-foreground/10 px-5 hover:bg-foreground/15"
data-testid="community-team-intro-back"
disabled={isPending || transaction.stage === "entering"}
onClick={backToProfile}
variant="ghost"
>
Back
</Button>
{transaction.error ? (
<Button
className="h-9 rounded-full bg-foreground/10 px-5 hover:bg-foreground/15"
disabled={isPending}
onClick={() => void finish()}
variant="ghost"
>
Skip for now
</Button>
) : null}
</OnboardingFooter>
</>
)}

View File

@@ -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();
const completionKey = `buzz-community-onboarding-complete.v1:${encodeURIComponent(COMMUNITY_RELAY_URL)}:${WELCOME_FAILURE_PUBKEY}`;
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();
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 ({

View File

@@ -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();