diff --git a/desktop/src/features/communities/ui/HostedCommunityOnboarding.tsx b/desktop/src/features/communities/ui/HostedCommunityOnboarding.tsx index cc4c263859..1152cd8fec 100644 --- a/desktop/src/features/communities/ui/HostedCommunityOnboarding.tsx +++ b/desktop/src/features/communities/ui/HostedCommunityOnboarding.tsx @@ -56,7 +56,26 @@ const MODAL_PRIMARY_ACTION_CLASS = `${ONBOARDING_PRIMARY_CTA_CLASS} !text-[rgb(v const MODAL_BACK_ACTION_CLASS = "h-9 rounded-full bg-foreground/10 px-6 hover:bg-foreground/15"; -export function HostedCommunityOnboarding({ onBack }: { onBack: () => void }) { +type HostedCommunityOnboardingProps = { + onBack: () => void; + /** + * Fires once the account is signed in with a linked identity — the parent + * uses this to reveal the stage page only after the sign-in modal has + * finished driving the flow. + */ + onReady?: () => void; + /** + * While true, render only the sign-in modal and keep the page scaffolding + * hidden, so whatever screen launched the flow stays visible behind it. + */ + stageHidden?: boolean; +}; + +export function HostedCommunityOnboarding({ + onBack, + onReady, + stageHidden = false, +}: HostedCommunityOnboardingProps) { const onboarding = useCommunityOnboarding(); const shouldReduceMotion = useReducedMotion(); const localPubkey = useIdentityQuery().data?.pubkey ?? null; @@ -310,6 +329,15 @@ export function HostedCommunityOnboarding({ onBack }: { onBack: () => void }) { const ready = Boolean(auth && identity && !identityMismatch); const modalOpen = !loading && !ready; + // Tell the parent once sign-in completes so it can reveal the stage page. + // Guarded so it fires exactly once per mount. + const readyNotifiedRef = React.useRef(false); + React.useEffect(() => { + if (loading || !ready || readyNotifiedRef.current) return; + readyNotifiedRef.current = true; + onReady?.(); + }, [loading, onReady, ready]); + const errorBox = error ? (
void }) { ); + const signInDialog = ( + { + if (open) return; + if (action === "Signing in…") { + cancelSignInAndGoBack(); + return; + } + if (!busy) goBack(); + }} + > + +
+ + + {!auth ? ( + <> + + Set up your community + + + Sign in to connect a community you already own or create a new + one. We’ll open Builderlab in your browser, then bring you back + to Buzz. + + {errorBox ?
{errorBox}
: null} + {action === "Signing in…" ? ( + + ) : ( + + )} + {/* Quiet breadcrumb: Buzz itself is open source; this hosted + relay is the one account-backed piece of the flow. */} +

+ Buzz is open source. Builderlab hosts the relay for this + account. +

+ + ) : !identity ? ( + <> + + Finish connecting Buzz + + + Your Builderlab account + {auth.email ? ` (${auth.email})` : ""} is ready. Connect this + device’s Buzz identity to finish setup. Your private key stays + on this device. + + {errorBox ?
{errorBox}
: null} + + + ) : ( + <> + + This account uses a different Buzz identity + + + This account is connected to another Buzz identity. Reconnect + this device, or sign out to use a different email. + +

+ Account: {identity.npub ?? boundPubkey} +
+ This device: {localNpub ?? localPubkey} +

+ {errorBox ?
{errorBox}
: null} +
+ + +
+ + )} +
+
+
+ ); + + // While the sign-in modal drives the flow, keep the launching page + // visible behind it instead of swapping to this stage prematurely. + if (stageHidden) { + return signInDialog; + } + return (

@@ -619,127 +771,7 @@ export function HostedCommunityOnboarding({ onBack }: { onBack: () => void }) { ) : null} - { - if (open) return; - if (action === "Signing in…") { - cancelSignInAndGoBack(); - return; - } - if (!busy) goBack(); - }} - > - -
- - - {!auth ? ( - <> - - Set up your community - - - Sign in to connect a community you already own or create a new - one. We’ll open Builderlab in your browser, then bring you - back to Buzz. - - {errorBox ? ( -
{errorBox}
- ) : null} - {action === "Signing in…" ? ( - - ) : ( - - )} - {/* Quiet breadcrumb: Buzz itself is open source; this hosted - relay is the one account-backed piece of the flow. */} -

- Buzz is open source. Builderlab hosts the relay for this - account. -

- - ) : !identity ? ( - <> - - Finish connecting Buzz - - - Your Builderlab account - {auth.email ? ` (${auth.email})` : ""} is ready. Connect this - device’s Buzz identity to finish setup. Your private key stays - on this device. - - {errorBox ? ( -
{errorBox}
- ) : null} - - - ) : ( - <> - - This account uses a different Buzz identity - - - This account is connected to another Buzz identity. Reconnect - this device, or sign out to use a different email. - -

- Account: {identity.npub ?? boundPubkey} -
- This device: {localNpub ?? localPubkey} -

- {errorBox ? ( -
{errorBox}
- ) : null} -
- - -
- - )} -
-
-
+ {signInDialog}

); } diff --git a/desktop/src/features/communities/ui/WelcomeSetup.tsx b/desktop/src/features/communities/ui/WelcomeSetup.tsx index 33edf14bb2..964e105f8f 100644 --- a/desktop/src/features/communities/ui/WelcomeSetup.tsx +++ b/desktop/src/features/communities/ui/WelcomeSetup.tsx @@ -1,36 +1,23 @@ import * as React from "react"; -import { Check, Copy } from "lucide-react"; +import { HostedCommunityOnboarding } from "@/features/communities/ui/HostedCommunityOnboarding"; import { useCommunityOnboarding } from "@/features/onboarding/communityOnboarding"; -import { normalizeRelayUrl } from "@/features/communities/relayProbe"; import { InviteRedeemForm } from "@/features/onboarding/ui/InviteRedeemForm"; +import { OnboardingChrome } from "@/features/onboarding/ui/OnboardingChrome"; import { - ONBOARDING_KEY_ROW_CLASS, - ONBOARDING_KEY_TEXT_CLASS, -} from "@/features/onboarding/ui/NsecMaskedDisplay"; + OnboardingFooter, + OnboardingFooterProvider, +} from "@/features/onboarding/ui/OnboardingFooter"; import { type OnboardingTransitionDirection, OnboardingSlideTransition, } from "@/features/onboarding/ui/OnboardingSlideTransition"; -import { - OnboardingFooter, - OnboardingFooterProvider, -} from "@/features/onboarding/ui/OnboardingFooter"; -import { getIdentity } from "@/shared/api/tauriIdentity"; -import { pubkeyToNpub } from "@/shared/lib/nostrUtils"; +import { useSystemColorScheme } from "@/shared/theme/useSystemColorScheme"; import { Button } from "@/shared/ui/button"; import { Card } from "@/shared/ui/card"; -import { Input } from "@/shared/ui/input"; import { StartupWindowDragRegion } from "@/shared/ui/StartupWindowDragRegion"; -import { useSystemColorScheme } from "@/shared/theme/useSystemColorScheme"; -import { - ONBOARDING_PRIMARY_CTA_CLASS, - OnboardingChrome, -} from "@/features/onboarding/ui/OnboardingChrome"; -import { HostedCommunityOnboarding } from "@/features/communities/ui/HostedCommunityOnboarding"; -import { writeTextToClipboard } from "@/shared/lib/clipboard"; -type WelcomeSetupPage = "welcome" | "join" | "invite" | "owned"; +type WelcomeSetupPage = "welcome" | "existing" | "join" | "member" | "owned"; type WelcomeTransitionMode = "initial" | OnboardingTransitionDirection; type WelcomeSetupProps = { @@ -41,15 +28,6 @@ type WelcomeSetupProps = { const COMMUNITY_OPTION_CARD_CLASS = "w-full max-w-[320px] items-center px-6 py-4 text-center text-sm font-normal leading-6 text-foreground [--buzz-card-textured-min-height:88px] transition-[filter] duration-150 ease-out hover:brightness-[0.98] focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-foreground/35"; -const WIDE_TEXTURE_CARD_CLASS = - "relative left-1/2 w-[min(calc(100%+10rem),calc(100vw-2rem))] max-w-[1040px] -translate-x-1/2 px-8 py-6 [--buzz-card-textured-min-height:192px]"; -const WIDE_TEXTURE_CONTENT_CLASS = "mx-auto w-full max-w-[840px]"; -const HORIZONTAL_INPUT_OVERFLOW_FADE = { - WebkitMaskImage: - "linear-gradient(to right, transparent, black 2rem, black calc(100% - 2rem), transparent)", - maskImage: - "linear-gradient(to right, transparent, black 2rem, black calc(100% - 2rem), transparent)", -}; export function WelcomeSetup({ initialPage = "welcome", @@ -59,63 +37,45 @@ export function WelcomeSetup({ const [page, setPage] = React.useState(initialPage); const [transitionMode, setTransitionMode] = React.useState(initialTransitionMode); - const [npub, setNpub] = React.useState(""); - const [identityError, setIdentityError] = React.useState(null); - const [relayUrl, setRelayUrl] = React.useState(""); - const [relayUrlError, setRelayUrlError] = React.useState(null); - const [copied, setCopied] = React.useState(false); + // While true, the Builderlab sign-in modal floats over the current page — + // we only navigate to the hosted stage once sign-in completes, so the page + // behind the modal never changes out from under the user. + const [isHostedSignInOpen, setIsHostedSignInOpen] = React.useState(false); const communityOnboarding = useCommunityOnboarding(); const systemColorScheme = useSystemColorScheme(); - React.useEffect(() => { - if (page !== "join" || npub || identityError) return; - void getIdentity() - .then((identity) => setNpub(pubkeyToNpub(identity.pubkey))) - .catch((error: unknown) => - setIdentityError( - error instanceof Error - ? error.message - : "Could not load your public key.", - ), + const showPage = React.useCallback( + (nextPage: WelcomeSetupPage, direction?: OnboardingTransitionDirection) => { + setTransitionMode( + direction ?? (nextPage === "welcome" ? "backward" : "forward"), ); - }, [identityError, npub, page]); - - const showPage = React.useCallback((nextPage: WelcomeSetupPage) => { - if (nextPage === "join") setIdentityError(null); - setTransitionMode(nextPage === "welcome" ? "backward" : "forward"); - setPage(nextPage); - }, []); - - const handleJoin = React.useCallback( - (event: React.FormEvent) => { - event.preventDefault(); - const normalizedRelayUrl = normalizeRelayUrl(relayUrl); - if (!normalizedRelayUrl) { - setRelayUrlError("Enter a valid community URL."); - return; - } + setPage(nextPage); + }, + [], + ); - setRelayUrlError(null); + const startConnection = React.useCallback( + (relayUrl: string) => { communityOnboarding.start({ source: "first-community", - firstCommunityPage: "join", - relayUrl: normalizedRelayUrl, + firstCommunityPage: page === "member" ? "member" : "join", + relayUrl, }); }, - [communityOnboarding, relayUrl], + [communityOnboarding, page], ); - const handleInviteRedeem = React.useCallback( - (relayWsUrl: string, code: string, policyReceipt?: string) => { + const redeemInvite = React.useCallback( + (relayUrl: string, code: string, policyReceipt?: string) => { communityOnboarding.start({ source: "first-community", - firstCommunityPage: "join", - relayUrl: relayWsUrl, + firstCommunityPage: page === "member" ? "member" : "join", + relayUrl, inviteCode: code, policyReceipt, }); }, - [communityOnboarding], + [communityOnboarding, page], ); const transitionDirection = @@ -145,8 +105,8 @@ export function WelcomeSetup({ Join or create a community

- Choose how you’d like to get started. If you have an invite - link, you can open it directly to continue setup. + Join with an invite, create your own community, or reconnect + one you already have.

@@ -155,8 +115,12 @@ export function WelcomeSetup({ className={COMMUNITY_OPTION_CARD_CLASS} variant="textured" > - - -
@@ -192,146 +162,53 @@ export function WelcomeSetup({ - ) : page === "owned" ? ( + ) : page === "existing" ? ( - showPage("welcome")} /> - - ) : page === "join" ? ( -
-

- Request access to a community +

+ Reconnect to your community

+

+ Tell us your role so we can find the fastest way back in. +

-
-
-
+ + -
-
- - {identityError ? ( -

- {identityError} -

- ) : null} - -
+ + + +
-
+ ) : page === "owned" ? ( + + showPage("welcome")} /> + ) : ( -
+

- Enter your invite link + {page === "member" + ? "Reconnect to your community" + : "Join a community"}

- If you have an invite link for a community, paste it below to - continue setup. + {page === "member" + ? "Enter the community URL or an invite link. Your role will be restored when you connect." + : "Enter the invite link or community URL you received."}

showPage("welcome")} - onRedeem={handleInviteRedeem} + onCancel={() => + showPage(page === "member" ? "existing" : "welcome") + } + onConnect={startConnection} + onRedeem={redeemInvite} + placeholder="Invite link or community URL" variant="onboarding-spotlight" />
)} + {isHostedSignInOpen && page !== "owned" ? ( + setIsHostedSignInOpen(false)} + onReady={() => { + setIsHostedSignInOpen(false); + showPage("owned"); + }} + stageHidden + /> + ) : null}
diff --git a/desktop/src/features/onboarding/communityOnboarding.tsx b/desktop/src/features/onboarding/communityOnboarding.tsx index e2215a79f8..76520b83be 100644 --- a/desktop/src/features/onboarding/communityOnboarding.tsx +++ b/desktop/src/features/onboarding/communityOnboarding.tsx @@ -26,7 +26,7 @@ export type CommunityOnboardingStage = */ | "entering"; -export type FirstCommunityPage = "join" | "owned"; +export type FirstCommunityPage = "join" | "member" | "owned"; export type CommunityOnboardingTransaction = { id: string; diff --git a/desktop/src/features/onboarding/ui/InviteRedeemForm.tsx b/desktop/src/features/onboarding/ui/InviteRedeemForm.tsx index 55026d2b57..de685fe52c 100644 --- a/desktop/src/features/onboarding/ui/InviteRedeemForm.tsx +++ b/desktop/src/features/onboarding/ui/InviteRedeemForm.tsx @@ -11,6 +11,7 @@ import { isJoinPolicyDiscoveryCandidate, type JoinPolicy, } from "@/shared/api/invites"; +import { normalizeRelayUrl } from "@/features/communities/relayProbe"; import { cn } from "@/shared/lib/cn"; import { Button } from "@/shared/ui/button"; import { Card } from "@/shared/ui/card"; @@ -45,7 +46,9 @@ type InviteRedeemFormProps = { error: string | null; isRedeeming: boolean; onCancel: () => void; + onConnect?: (relayWsUrl: string) => void; onRedeem: (relayWsUrl: string, code: string, policyReceipt?: string) => void; + placeholder?: string; variant?: "default" | "onboarding-spotlight"; }; @@ -54,7 +57,9 @@ export function InviteRedeemForm({ error, isRedeeming, onCancel, + onConnect, onRedeem, + placeholder, variant = "default", }: InviteRedeemFormProps) { const formId = React.useId(); @@ -77,14 +82,22 @@ export function InviteRedeemForm({ () => parseInviteInput(inviteInput), [inviteInput], ); - const isBareCode = parsed !== null && !("relayWsUrl" in parsed); + const normalizedRelayUrl = React.useMemo( + () => (onConnect && !parsed ? normalizeRelayUrl(inviteInput) : null), + [inviteInput, onConnect, parsed], + ); + const parsedInvite = parsed; + const isBareCode = parsedInvite !== null && !("relayWsUrl" in parsedInvite); const needsRelayField = isBareCode && defaultRelayUrl !== undefined; React.useEffect(() => { - if (!parsed) return; + if (!parsedInvite) return; const relayWsUrl = - "relayWsUrl" in parsed ? parsed.relayWsUrl : bareCodeRelayUrl.trim(); + "relayWsUrl" in parsedInvite && + typeof parsedInvite.relayWsUrl === "string" + ? parsedInvite.relayWsUrl + : bareCodeRelayUrl.trim(); if (!relayWsUrl || !isJoinPolicyDiscoveryCandidate(relayWsUrl)) return; let cancelled = false; @@ -93,7 +106,7 @@ export function InviteRedeemForm({ .then((policy) => { if (cancelled || !policy) return; setJoinPolicy(policy); - setPolicyInvite({ relayWsUrl, code: parsed.code }); + setPolicyInvite({ relayWsUrl, code: parsedInvite.code }); setAgeConfirmed(false); setAgreementConfirmed(false); setPolicyError(null); @@ -108,12 +121,13 @@ export function InviteRedeemForm({ cancelled = true; window.clearTimeout(timeoutId); }; - }, [bareCodeRelayUrl, parsed]); + }, [bareCodeRelayUrl, parsedInvite]); const canSubmit = - parsed !== null && - ("relayWsUrl" in parsed || - (isBareCode && bareCodeRelayUrl.trim().length > 0)); + (parsedInvite !== null && + ("relayWsUrl" in parsedInvite || + (isBareCode && bareCodeRelayUrl.trim().length > 0))) || + normalizedRelayUrl !== null; const isOnboardingSpotlight = variant === "onboarding-spotlight"; const showInvalidInviteTip = isOnboardingSpotlight && inviteInput.trim().length > 0 && !canSubmit; @@ -121,10 +135,16 @@ export function InviteRedeemForm({ const handleSubmit = React.useCallback( async (event: React.FormEvent) => { event.preventDefault(); - if (!parsed) return; + if (normalizedRelayUrl) { + onConnect?.(normalizedRelayUrl); + return; + } + if (!parsedInvite) return; const relayWsUrl = - "relayWsUrl" in parsed ? parsed.relayWsUrl : bareCodeRelayUrl.trim(); + "relayWsUrl" in parsedInvite + ? parsedInvite.relayWsUrl + : bareCodeRelayUrl.trim(); if (!relayWsUrl) return; setPolicyError(null); @@ -132,7 +152,7 @@ export function InviteRedeemForm({ try { const policy = await getJoinPolicy(relayWsUrl); if (!policy) { - onRedeem(relayWsUrl, parsed.code); + onRedeem(relayWsUrl, parsedInvite.code); return; } @@ -140,10 +160,10 @@ export function InviteRedeemForm({ !joinPolicy || joinPolicy.version !== policy.version || policyInvite?.relayWsUrl !== relayWsUrl || - policyInvite.code !== parsed.code + policyInvite.code !== parsedInvite.code ) { setJoinPolicy(policy); - setPolicyInvite({ relayWsUrl, code: parsed.code }); + setPolicyInvite({ relayWsUrl, code: parsedInvite.code }); setAgeConfirmed(false); setAgreementConfirmed(false); return; @@ -163,11 +183,11 @@ export function InviteRedeemForm({ const receipt = await acceptJoinPolicy( relayWsUrl, - parsed.code, + parsedInvite.code, policy.version, ageConfirmed, ); - onRedeem(relayWsUrl, parsed.code, receipt); + onRedeem(relayWsUrl, parsedInvite.code, receipt); } catch (policyFetchError) { setPolicyError(inviteErrorMessage(policyFetchError)); } finally { @@ -180,7 +200,9 @@ export function InviteRedeemForm({ bareCodeRelayUrl, joinPolicy, onRedeem, - parsed, + normalizedRelayUrl, + onConnect, + parsedInvite, policyInvite, ], ); @@ -292,7 +314,9 @@ export function InviteRedeemForm({ disabled={isRedeeming} id="invite-input" onChange={handleInviteInputChange} - placeholder="https://relay.example.com/invite/abc123" + placeholder={ + placeholder ?? "https://relay.example.com/invite/abc123" + } spellCheck={false} type="text" value={inviteInput} @@ -336,7 +360,7 @@ export function InviteRedeemForm({ )} data-testid="invalid-invite-tip" > - Please enter a valid invite link + Please enter a valid invite link or community URL

) : null} diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts index 50985cc6c1..a7b0658588 100644 --- a/desktop/src/testing/e2eBridge.ts +++ b/desktop/src/testing/e2eBridge.ts @@ -10489,6 +10489,10 @@ export function maybeInstallE2eTauriMocks() { } case "agent_metric_archive_default_enabled": return activeConfig?.mock?.agentMetricArchiveDefaultEnabled ?? false; + case "set_prevent_sleep_active": + return null; + case "plugin:window|is_fullscreen": + return false; case "merge_save_subscription_kinds": { // Mirrors `merge_owner_p_kinds`: union `kind` into the owner_p row's // kinds, creating the row if it doesn't exist yet. diff --git a/desktop/tests/e2e/onboarding.spec.ts b/desktop/tests/e2e/onboarding.spec.ts index 9f1ddece86..86e22189e4 100644 --- a/desktop/tests/e2e/onboarding.spec.ts +++ b/desktop/tests/e2e/onboarding.spec.ts @@ -1,6 +1,6 @@ import { hexToBytes } from "@noble/hashes/utils.js"; -import { expect, test, type Locator, type Page } from "@playwright/test"; -import { npubEncode, nsecEncode } from "nostr-tools/nip19"; +import { expect, test, type Page } from "@playwright/test"; +import { nsecEncode } from "nostr-tools/nip19"; import { installMockBridge, TEST_IDENTITIES } from "../helpers/bridge"; import { installFakeCamera } from "../helpers/fakeCamera"; @@ -101,21 +101,6 @@ async function expectNoHomeSeenEntries(page: Page) { await expect.poll(async () => readHomeSeenStorageKeys(page)).toEqual([]); } -async function expectCommunityBranchFramePosition(page: Page, frame: Locator) { - const box = await frame.boundingBox(); - const viewport = page.viewportSize(); - if (!box || !viewport) { - throw new Error("Could not measure community branch frame position"); - } - const chromeOffset = 106; - const footerOffset = 144; - const frameCenterY = box.y + box.height / 2; - const usableLaneCenterY = - chromeOffset + (viewport.height - chromeOffset - footerOffset) / 2; - expect(frameCenterY).toBeGreaterThan(usableLaneCenterY); - expect(box.y + box.height).toBeLessThan(viewport.height - footerOffset); -} - async function selectFirstEmojiFromPicker(page: Page) { const picker = page.locator("em-emoji-picker"); await expect(picker).toBeVisible(); @@ -575,7 +560,7 @@ test("first-launch key import continues to machine setup", async ({ page }) => { await expect(page.getByTestId("app-loading-gate")).toHaveCount(0); }); -test("first-community choices expose npub and invite input", async ({ +test("first-community choices route join, create, owner, and member intents", async ({ page, }) => { await seedActiveIdentity(page, BLANK_TYLER_IDENTITY); @@ -590,155 +575,54 @@ test("first-community choices expose npub and invite input", async ({ skipOnboardingSeed: true, skipCommunitySeed: true, }); - await page.route( - "https://default.example.com/api/join-policy", - async (route) => { - await route.fulfill({ - status: 200, - contentType: "application/json", - body: "{}", - }); - }, - ); await page.goto("/"); await expect( - page.getByRole("button", { name: "Add me to a community" }), + page.getByRole("button", { name: /Join a community/ }), ).toBeVisible(); await expect( - page.getByRole("button", { name: "I have an invite link" }), + page.getByRole("button", { name: /Create a community/ }), ).toBeVisible(); + const existing = page.getByRole("button", { + name: /I already have a community/, + }); + await expect(existing).toBeVisible(); + await existing.click(); + // Owner/member split lives on its own page, mirroring the hub layout. await expect( - page.getByRole("button", { - name: "Create or connect to my own community", - }), + page.getByRole("heading", { name: "Reconnect to your community" }), ).toBeVisible(); - await page - .getByRole("button", { name: "Create or connect to my own community" }) - .click(); - // Sign-in opens as a modal over the blurred hosted-community preview. await expect( - page.getByRole("heading", { name: "Set up your community" }), + page.getByRole("button", { name: "I own the community" }), ).toBeVisible(); await expect( - page.getByRole("button", { name: "Sign in to continue" }), + page.getByRole("button", { name: "I’m a member or admin" }), ).toBeVisible(); - // The modal's close (X) dismisses back to the community picker. - await page.getByRole("button", { name: "Close" }).click(); - await page.getByRole("button", { name: "Add me to a community" }).click(); - await expect(page.getByTestId("welcome-join-npub")).toHaveText( - npubEncode(BLANK_TYLER_IDENTITY.pubkey), - ); - const joinKeyFrame = page.getByTestId("welcome-join-npub-frame"); - const joinNpub = page.getByTestId("welcome-join-npub"); - await expect(joinKeyFrame).toBeVisible(); - await expect(joinNpub).toBeVisible(); - await expectCommunityBranchFramePosition(page, joinKeyFrame); - const joinKeyFrameBox = await joinKeyFrame.boundingBox(); - expect(joinKeyFrameBox?.width).toBeGreaterThan(700); - await expect(joinKeyFrame).toHaveClass(/buzz-card-textured/); - const joinKeyFrameStyles = await joinKeyFrame.evaluate((element) => { - const styles = window.getComputedStyle(element); - return { - backgroundColor: styles.backgroundColor, - borderRadius: styles.borderRadius, - }; - }); - expect(joinKeyFrameStyles.backgroundColor).toBe("rgba(0, 0, 0, 0)"); - expect(joinKeyFrameStyles.borderRadius).toBe("0px"); - await expect - .poll(() => - joinNpub.evaluate((element) => { - const styles = window.getComputedStyle(element); - return { - color: styles.color, - fontFamily: styles.fontFamily, - fontSize: styles.fontSize, - }; - }), - ) - .toMatchObject({ - color: "rgb(113, 113, 6)", - fontSize: "36px", - }); - expect( - ( - await joinNpub.evaluate((element) => - window.getComputedStyle(element).fontFamily.toLowerCase(), - ) - ).includes("mono"), - ).toBe(true); + await page.getByRole("button", { name: "I’m a member or admin" }).click(); + await expect( + page.getByRole("heading", { name: "Reconnect to your community" }), + ).toBeVisible(); + const accessInput = page.getByTestId("invite-redeem-input"); + await expect(accessInput).toHaveAttribute( + "placeholder", + "Invite link or community URL", + ); + await accessInput.fill("https://default.example.com"); + await expect(page.getByTestId("invite-redeem-submit")).toBeEnabled(); + // Back from the member form returns to the role choice, then to the hub. await page.getByRole("button", { name: "Back" }).click(); + await expect( + page.getByRole("button", { name: "I own the community" }), + ).toBeVisible(); + await page.getByTestId("existing-back").click(); - await page.getByRole("button", { name: "I have an invite link" }).click(); + await page.getByRole("button", { name: /Join a community/ }).click(); await expect( - page.getByRole("heading", { name: "Enter your invite link" }), + page.getByRole("heading", { name: "Join a community" }), ).toBeVisible(); - const inviteInputFrame = page.getByTestId("invite-redeem-input-frame"); - const inviteInput = page.getByTestId("invite-redeem-input"); - await expect(inviteInputFrame).toBeVisible(); - await expect(inviteInput).toBeVisible(); - await expectCommunityBranchFramePosition(page, inviteInputFrame); - const inviteInputFrameBox = await inviteInputFrame.boundingBox(); - expect(inviteInputFrameBox?.width).toBeGreaterThan(700); - await expect(inviteInputFrame).toHaveClass(/buzz-card-textured/); - const inviteInputFrameStyles = await inviteInputFrame.evaluate((element) => { - const styles = window.getComputedStyle(element); - return { - backgroundColor: styles.backgroundColor, - borderRadius: styles.borderRadius, - }; - }); - expect(inviteInputFrameStyles.backgroundColor).toBe("rgba(0, 0, 0, 0)"); - expect(inviteInputFrameStyles.borderRadius).toBe("0px"); - await expect - .poll(() => - inviteInput.evaluate((element) => { - const styles = window.getComputedStyle(element); - return { - color: styles.color, - fontFamily: styles.fontFamily, - fontSize: styles.fontSize, - }; - }), - ) - .toMatchObject({ - color: "rgb(113, 113, 6)", - fontSize: "36px", - }); - expect( - ( - await inviteInput.evaluate((element) => - window.getComputedStyle(element).fontFamily.toLowerCase(), - ) - ).includes("mono"), - ).toBe(true); - await expect(page.getByTestId("invite-redeem-submit")).toHaveText("Next"); - await expect(page.getByTestId("invite-redeem-submit")).toBeDisabled(); - const inviteFrame = page.getByTestId("invite-redeem-input-frame"); - const initialInviteFrameBox = await inviteFrame.boundingBox(); - expect(initialInviteFrameBox).not.toBeNull(); - const invalidInviteTip = page.getByTestId("invalid-invite-tip"); - await expect(invalidInviteTip).toHaveAttribute("aria-hidden", "true"); - await expect(invalidInviteTip).toHaveCSS("opacity", "0"); - - await inviteInput.fill("awefi"); - await expect(page.getByLabel("Relay URL")).toHaveCount(0); - await expect(page.getByTestId("invite-redeem-submit")).toBeDisabled(); - await expect(invalidInviteTip).toBeVisible(); - await expect(invalidInviteTip).toHaveAttribute("aria-hidden", "false"); - await expect(invalidInviteTip).toHaveCSS("opacity", "1"); - await expect(invalidInviteTip).toHaveCSS("color", "rgb(113, 113, 6)"); - await expect(invalidInviteTip).toHaveCSS("text-align", "center"); - const invalidInviteFrameBox = await inviteFrame.boundingBox(); - expect(invalidInviteFrameBox?.y).toBe(initialInviteFrameBox?.y); - - await inviteInput.fill("https://default.example.com/invite/abc123"); - await expect(page.getByLabel("Relay URL")).toHaveCount(0); + await accessInput.fill("https://default.example.com/invite/abc123"); await expect(page.getByTestId("invite-redeem-submit")).toBeEnabled(); - await expect(invalidInviteTip).toHaveAttribute("aria-hidden", "true"); - await expect(invalidInviteTip).toHaveCSS("opacity", "0"); }); test("first-community owner can connect an existing hosted community", async ({ @@ -775,9 +659,7 @@ test("first-community owner can connect an existing hosted community", async ({ ); await page.goto("/"); - await page - .getByRole("button", { name: "Create or connect to my own community" }) - .click(); + await page.getByTestId("community-choice-create").click(); await expect(page.getByText("North Star")).toBeVisible(); await page.getByRole("button", { name: "Connect", exact: true }).click(); await expect( @@ -803,7 +685,7 @@ test("first-community owner can connect an existing hosted community", async ({ ).toBeVisible(); await expect(page.getByText("North Star")).toBeVisible(); await expect( - page.getByRole("heading", { name: "Request access to community" }), + page.getByRole("heading", { name: "Join a community" }), ).toHaveCount(0); await expect .poll(() => @@ -835,9 +717,7 @@ test("first-community owner can create and connect a hosted community", async ({ ); await page.goto("/"); - await page - .getByRole("button", { name: "Create or connect to my own community" }) - .click(); + await page.getByTestId("community-choice-create").click(); await page.getByRole("button", { name: "Sign in to continue" }).click(); await expect( page.getByRole("heading", { name: "Finish connecting Buzz" }), @@ -913,9 +793,7 @@ test("hosted community address line stays within the card for a long name", asyn await page.setViewportSize({ width: 800, height: 720 }); await page.goto("/"); - await page - .getByRole("button", { name: "Create or connect to my own community" }) - .click(); + await page.getByTestId("community-choice-create").click(); await page.getByRole("button", { name: "Sign in to continue" }).click(); await expect( page.getByRole("heading", { name: "Finish connecting Buzz" }), @@ -984,9 +862,7 @@ test("first-community reports a created community without a relay address", asyn ); await page.goto("/"); - await page - .getByRole("button", { name: "Create or connect to my own community" }) - .click(); + await page.getByTestId("community-choice-create").click(); await page.getByRole("textbox", { name: "Community name" }).fill("bee-lab"); await expect(page.getByText("That address is available.")).toBeVisible(); await page.getByRole("button", { name: "Next" }).click(); @@ -1017,9 +893,7 @@ test("first-community X cancels a pending sign-in", async ({ page }) => { ); await page.goto("/"); - await page - .getByRole("button", { name: "Create or connect to my own community" }) - .click(); + await page.getByTestId("community-choice-create").click(); await page.getByRole("button", { name: "Sign in to continue" }).click(); await expect(page.getByText("Waiting for your browser…")).toBeVisible(); await expect( @@ -1027,7 +901,7 @@ test("first-community X cancels a pending sign-in", async ({ page }) => { ).toHaveCount(0); await page.getByRole("button", { name: "Close" }).click(); await expect( - page.getByRole("button", { name: "Create or connect to my own community" }), + page.getByRole("button", { name: /Create a community/ }), ).toBeVisible(); await expect .poll(() => page.evaluate(() => window.__BUZZ_E2E_COMMANDS__ ?? [])) @@ -1061,9 +935,7 @@ test("first-community owner can replace a mismatched account identity", async ({ ); await page.goto("/"); - await page - .getByRole("button", { name: "Create or connect to my own community" }) - .click(); + await page.getByTestId("community-choice-create").click(); await expect( page.getByRole("heading", { name: "This account uses a different Buzz identity", @@ -1113,9 +985,7 @@ test("first-community explains when the local identity belongs to another accoun ); await page.goto("/"); - await page - .getByRole("button", { name: "Create or connect to my own community" }) - .click(); + await page.getByTestId("community-choice-create").click(); await page .getByRole("button", { name: "Use this device's identity" }) .click(); @@ -1156,13 +1026,9 @@ test("back clears Builderlab auth before returning to first-community choices", ); await page.goto("/"); - await page - .getByRole("button", { name: "Create or connect to my own community" }) - .click(); + await page.getByTestId("community-choice-create").click(); await page.getByRole("button", { name: "Back" }).click(); - await page - .getByRole("button", { name: "Create or connect to my own community" }) - .click(); + await page.getByTestId("community-choice-create").click(); await expect(page.getByRole("button", { name: "Continue" })).toBeVisible(); }); @@ -1187,14 +1053,11 @@ test("first-community shows the scenario cards for localhost", async ({ page.getByRole("button", { name: "Join default community" }), ).toHaveCount(0); await expect( - page.getByRole("button", { name: "Add me to a community" }), - ).toBeVisible(); - await expect( - page.getByRole("button", { name: "I have an invite link" }), + page.getByRole("button", { name: /Join a community/ }), ).toBeVisible(); await expect( page.getByRole("button", { - name: "Create or connect to my own community", + name: /Create a community/, }), ).toBeVisible(); @@ -1222,11 +1085,11 @@ test("first-community direct join reaches profile", async ({ page }) => { }); await page.goto("/"); - await page.getByRole("button", { name: "Add me to a community" }).click(); + await page.getByRole("button", { name: /Join a community/ }).click(); await page - .getByTestId("welcome-join-community-url") + .getByTestId("invite-redeem-input") .fill("wss://onboarding.communities.buzz.xyz"); - await page.getByRole("button", { name: "Join community" }).click(); + await page.getByTestId("invite-redeem-submit").click(); await expect( page.getByRole("heading", { name: "Build your profile" }), @@ -1278,16 +1141,16 @@ test("first-community direct join cancel returns to request access", async ({ ); await page.goto("/"); - await page.getByRole("button", { name: "Add me to a community" }).click(); + await page.getByRole("button", { name: /Join a community/ }).click(); await page - .getByTestId("welcome-join-community-url") + .getByTestId("invite-redeem-input") .fill("wss://onboarding.communities.buzz.xyz"); - await page.getByRole("button", { name: "Join community" }).click(); + await page.getByTestId("invite-redeem-submit").click(); await expect(page.getByText("Connecting securely…")).toBeVisible(); await page.getByRole("button", { name: "Cancel" }).click(); await expect( - page.getByRole("heading", { name: "Request access to community" }), + page.getByRole("heading", { name: "Join a community" }), ).toBeVisible(); await expect(page.getByTestId("community-change-overlay")).toHaveCount(0); await expect(page.getByText("Create an identity key")).toHaveCount(0); @@ -1658,7 +1521,7 @@ test("connected first-community profile step offers equal-width Next and Back co await backButton.click(); await expect( - page.getByRole("heading", { name: "Request access to community" }), + page.getByRole("heading", { name: "Join a community" }), ).toBeVisible(); await expect .poll(() =>