mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
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:
@@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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 ({
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user