mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(desktop): modal sign-in + branded communities onboarding page (#2147)
Signed-off-by: Bradley Axen <baxen@squareup.com> Signed-off-by: npub19x6jnl6rhepymwyl2xlltz3ce7rfg2ktllle3g2vu59n3s490k8s9n40l3 <29b529ff43be424db89f51bff58a38cf86942acbffff98a14ce50b38c2a57d8f@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: npub19x6jnl6rhepymwyl2xlltz3ce7rfg2ktllle3g2vu59n3s490k8s9n40l3 <29b529ff43be424db89f51bff58a38cf86942acbffff98a14ce50b38c2a57d8f@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
co-authored by
Claude Opus 4.8
npub19x6jnl6rhepymwyl2xlltz3ce7rfg2ktllle3g2vu59n3s490k8s9n40l3
parent
1a94146181
commit
8a65d81fab
@@ -17,7 +17,10 @@ import { useReloadShortcut } from "@/app/useReloadShortcut";
|
||||
import { KnownAgentPubkeysProvider } from "@/features/agents/useKnownAgentPubkeys";
|
||||
import { useAppOnboardingState } from "@/features/onboarding/hooks";
|
||||
import { useMachineOnboardingState } from "@/features/onboarding/machineOnboarding";
|
||||
import { useCommunityOnboarding } from "@/features/onboarding/communityOnboarding";
|
||||
import {
|
||||
type FirstCommunityPage,
|
||||
useCommunityOnboarding,
|
||||
} from "@/features/onboarding/communityOnboarding";
|
||||
import { CommunityOnboardingFlow } from "@/features/onboarding/ui/CommunityOnboardingFlow";
|
||||
import {
|
||||
MachineOnboardingFlow,
|
||||
@@ -275,8 +278,8 @@ function CommunityApp({
|
||||
const communityOnboarding = useCommunityOnboarding();
|
||||
const connectingTransactionRef = useRef<string | null>(null);
|
||||
const [isCommunityChangeOpen, setIsCommunityChangeOpen] = useState(false);
|
||||
const [resumeFirstCommunityJoin, setResumeFirstCommunityJoin] =
|
||||
useState(false);
|
||||
const [resumeFirstCommunityPage, setResumeFirstCommunityPage] =
|
||||
useState<FirstCommunityPage | null>(null);
|
||||
|
||||
// Surface nest-related backend events (repos-dir errors, legacy migration)
|
||||
// as toasts. Mounted before useCommunityInit so the listeners are registered
|
||||
@@ -354,7 +357,7 @@ function CommunityApp({
|
||||
}
|
||||
if (communities.length === 1) {
|
||||
if (transaction.source === "first-community") {
|
||||
setResumeFirstCommunityJoin(true);
|
||||
setResumeFirstCommunityPage(transaction.firstCommunityPage ?? "join");
|
||||
}
|
||||
clearCommunities();
|
||||
return;
|
||||
@@ -410,7 +413,7 @@ function CommunityApp({
|
||||
// Show welcome setup for first-run users with no communities
|
||||
appContent = (
|
||||
<WelcomeSetup
|
||||
initialPage={resumeFirstCommunityJoin ? "join" : undefined}
|
||||
initialPage={resumeFirstCommunityPage ?? undefined}
|
||||
onBack={onBackToMachineConfig}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as React from "react";
|
||||
import { AlertCircle, CheckCircle2, LoaderCircle } from "lucide-react";
|
||||
import { AlertCircle, LoaderCircle } from "lucide-react";
|
||||
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
||||
|
||||
import {
|
||||
bindBuilderlabIdentity,
|
||||
@@ -24,17 +25,47 @@ import { useCommunityOnboarding } from "@/features/onboarding/communityOnboardin
|
||||
import { useIdentityQuery } from "@/shared/api/hooks";
|
||||
import { safeNpub } from "@/shared/lib/nostrUtils";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Card } from "@/shared/ui/card";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { OnboardingFooter } from "@/features/onboarding/ui/OnboardingFooter";
|
||||
import {
|
||||
ONBOARDING_INK_ICON_CLASS,
|
||||
ONBOARDING_PRIMARY_CTA_CLASS,
|
||||
} from "@/features/onboarding/ui/OnboardingChrome";
|
||||
import { BuzzMark } from "@/shared/ui/buzz-logo/BuzzMark";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogTitle,
|
||||
} from "@/shared/ui/dialog";
|
||||
|
||||
const FUZZY_SURFACE_CLASS =
|
||||
"relative left-1/2 w-[min(calc(100%+12rem),calc(100vw-2rem))] max-w-[1040px] -translate-x-1/2 px-20 pb-14 pt-20 !text-[rgb(var(--buzz-hosted-community-surface-fg))] [--buzz-card-textured-min-height:224px]";
|
||||
const COMMUNITY_LIST_CLASS = "mx-auto w-full max-w-[520px] text-left";
|
||||
const COMMUNITY_ROW_CLASS =
|
||||
"flex min-h-[5.75rem] items-center justify-between gap-8 py-4 text-sm";
|
||||
const COMMUNITY_DIVIDER_CLASS =
|
||||
"border-b-[0.5px] border-[rgb(var(--buzz-hosted-community-divider-border)/0.5)]";
|
||||
const COMMUNITY_ACTION_CLASS =
|
||||
"h-[2.375rem] min-w-32 shrink-0 rounded-full bg-[rgb(var(--buzz-hosted-community-action-bg))] px-6 text-sm text-foreground shadow-none hover:bg-[rgb(var(--buzz-hosted-community-action-bg-hover))]";
|
||||
const PAGE_CTA_CLASS = `${ONBOARDING_PRIMARY_CTA_CLASS} w-36 shadow-none`;
|
||||
const PAGE_BACK_CLASS =
|
||||
"h-[2.375rem] w-36 rounded-full bg-foreground/10 px-6 shadow-none hover:bg-foreground/15";
|
||||
const MODAL_PRIMARY_ACTION_CLASS = `${ONBOARDING_PRIMARY_CTA_CLASS} !text-[rgb(var(--buzz-hosted-community-modal-action-fg))]`;
|
||||
const MODAL_BACK_ACTION_CLASS =
|
||||
"h-9 rounded-full bg-foreground/10 px-6 hover:bg-foreground/15";
|
||||
|
||||
export function HostedCommunityOnboarding({ onBack }: { onBack: () => void }) {
|
||||
const onboarding = useCommunityOnboarding();
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
const localPubkey = useIdentityQuery().data?.pubkey ?? null;
|
||||
const [auth, setAuth] = React.useState<BuilderlabAuth | null>(null);
|
||||
const [identity, setIdentity] = React.useState<HostedNostrIdentity | null>(
|
||||
null,
|
||||
);
|
||||
const [communities, setCommunities] = React.useState<HostedCommunity[]>([]);
|
||||
const [showCreate, setShowCreate] = React.useState(false);
|
||||
const [name, setName] = React.useState("");
|
||||
const [availability, setAvailability] = React.useState<boolean | null>(null);
|
||||
const [checkingName, setCheckingName] = React.useState(false);
|
||||
@@ -100,12 +131,14 @@ export function HostedCommunityOnboarding({ onBack }: { onBack: () => void }) {
|
||||
});
|
||||
};
|
||||
|
||||
const cancelSignIn = () => {
|
||||
const cancelSignInAndGoBack = () => {
|
||||
loginAttempt.current += 1;
|
||||
setAction(null);
|
||||
setError(null);
|
||||
void cancelBuilderlabLogin().catch((cause) => {
|
||||
setError(cause instanceof Error ? cause.message : String(cause));
|
||||
onBack();
|
||||
void cancelBuilderlabLogin().catch(() => {
|
||||
// The modal has already closed and the login attempt is invalidated;
|
||||
// cancellation is best-effort cleanup for the native/browser flow.
|
||||
});
|
||||
};
|
||||
|
||||
@@ -115,6 +148,7 @@ export function HostedCommunityOnboarding({ onBack }: { onBack: () => void }) {
|
||||
setAuth(null);
|
||||
setIdentity(null);
|
||||
setCommunities([]);
|
||||
setShowCreate(false);
|
||||
setName("");
|
||||
setAvailability(null);
|
||||
});
|
||||
@@ -188,6 +222,7 @@ export function HostedCommunityOnboarding({ onBack }: { onBack: () => void }) {
|
||||
normalizedName.length <= 63 &&
|
||||
VALID_HOSTED_COMMUNITY_NAME.test(normalizedName);
|
||||
const atCommunityLimit = communities.length >= HOSTED_COMMUNITY_LIMIT;
|
||||
const hasCommunities = activeCommunities.length > 0;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!identity || identityMismatch || !normalizedName || !validName) {
|
||||
@@ -228,6 +263,7 @@ export function HostedCommunityOnboarding({ onBack }: { onBack: () => void }) {
|
||||
if (
|
||||
!onboarding.start({
|
||||
source: "first-community",
|
||||
firstCommunityPage: "owned",
|
||||
relayUrl,
|
||||
communityName: community.name ?? community.slug,
|
||||
})
|
||||
@@ -268,231 +304,442 @@ export function HostedCommunityOnboarding({ onBack }: { onBack: () => void }) {
|
||||
};
|
||||
|
||||
const busy = action !== null;
|
||||
// The account is set up once we're signed in with a linked, matching
|
||||
// identity. Until then the sign-in / link-identity modal drives the flow and
|
||||
// the page behind it shows a blurred preview of where communities will land.
|
||||
const ready = Boolean(auth && identity && !identityMismatch);
|
||||
const modalOpen = !loading && !ready;
|
||||
|
||||
const errorBox = error ? (
|
||||
<div
|
||||
className="flex items-start gap-3 rounded-xl border border-destructive/30 bg-destructive/10 p-4 text-left"
|
||||
role="alert"
|
||||
>
|
||||
<span className="mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-destructive/15 text-destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
</span>
|
||||
<span className="text-sm font-medium leading-5 text-destructive">
|
||||
{error}
|
||||
</span>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
const creationFeedback = atCommunityLimit
|
||||
? `You’ve reached the limit of ${HOSTED_COMMUNITY_LIMIT} hosted communities.`
|
||||
: name && !validName
|
||||
? "Use lowercase letters, numbers, and single hyphens."
|
||||
: checkingName
|
||||
? "Checking availability…"
|
||||
: availability === false
|
||||
? "That address is already taken."
|
||||
: availability === true
|
||||
? "That address is available."
|
||||
: null;
|
||||
|
||||
// The composed `<name>.<suffix>` line renders at text-4xl, but a valid name
|
||||
// can be up to 63 chars and the suffix adds another 21 — far wider than the
|
||||
// card at the 800px app minimum. Scale the font down to fit the container
|
||||
// (container-query width) while capping at text-4xl (2.25rem) so short names
|
||||
// keep the full-size treatment and long names stay fully visible instead of
|
||||
// overflowing the surface.
|
||||
const composedAddressLength =
|
||||
(name ? name.length : "your-community".length) +
|
||||
HOSTED_COMMUNITY_SUFFIX.length +
|
||||
1; // leading dot before the suffix
|
||||
// ~0.62em is the monospace glyph advance; 90cqw leaves a safety margin so the
|
||||
// glyphs never touch the card edge.
|
||||
const addressFontSize = `min(2.25rem, calc(90cqw / ${(
|
||||
composedAddressLength * 0.62
|
||||
).toFixed(2)}))`;
|
||||
|
||||
const creationInput = (inline: boolean) => (
|
||||
<Input
|
||||
aria-describedby={
|
||||
creationFeedback ? "hosted-community-feedback" : undefined
|
||||
}
|
||||
aria-label="Community name"
|
||||
autoComplete="off"
|
||||
className={
|
||||
inline
|
||||
? "h-[2.375rem] w-[16.5rem] rounded-full border border-[color:var(--buzz-onboarding-backup-ink)]/25 bg-[rgb(var(--buzz-hosted-community-input-bg)/0.6)] px-6 text-center text-sm shadow-none placeholder:text-foreground/30 focus-visible:ring-1 focus-visible:ring-[color:var(--buzz-onboarding-backup-ink)]/40"
|
||||
: "h-auto min-w-0 flex-none rounded-none border-0 bg-transparent p-0 text-right font-mono !text-[rgb(var(--buzz-hosted-community-surface-fg))] shadow-none placeholder:!text-[rgb(var(--buzz-hosted-community-surface-fg))] placeholder:opacity-20 focus-visible:ring-0"
|
||||
}
|
||||
disabled={busy || atCommunityLimit}
|
||||
id="hosted-community-address"
|
||||
data-testid={!inline ? "hosted-community-address-input" : undefined}
|
||||
maxLength={63}
|
||||
onChange={(event) => {
|
||||
setName(event.target.value.toLowerCase());
|
||||
setAvailability(null);
|
||||
}}
|
||||
placeholder={inline ? "Community name here" : "your-community"}
|
||||
spellCheck={false}
|
||||
style={
|
||||
inline
|
||||
? undefined
|
||||
: {
|
||||
width: `${name ? name.length : "your-community".length}ch`,
|
||||
fontSize: addressFontSize,
|
||||
}
|
||||
}
|
||||
value={name}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderCreationForm = (inline: boolean) =>
|
||||
inline ? (
|
||||
<form
|
||||
className="relative"
|
||||
id="hosted-community-create-form"
|
||||
onSubmit={create}
|
||||
>
|
||||
<div className={COMMUNITY_ROW_CLASS}>
|
||||
<label className="text-sm" htmlFor="hosted-community-address">
|
||||
Set new community name
|
||||
</label>
|
||||
{creationInput(true)}
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<Card
|
||||
asChild
|
||||
className={`${FUZZY_SURFACE_CLASS} !py-10 [--buzz-card-textured-min-height:176px]`}
|
||||
data-testid="hosted-community-create-surface"
|
||||
variant="textured"
|
||||
>
|
||||
<form id="hosted-community-create-form" onSubmit={create}>
|
||||
<div
|
||||
className="mx-auto flex w-full max-w-[900px] items-center justify-center whitespace-nowrap"
|
||||
data-testid="hosted-community-address-line"
|
||||
style={{ containerType: "inline-size" }}
|
||||
>
|
||||
{creationInput(false)}
|
||||
<span
|
||||
className="shrink-0 font-mono !text-[rgb(var(--buzz-hosted-community-surface-fg))]"
|
||||
id="hosted-community-suffix"
|
||||
style={{ fontSize: addressFontSize }}
|
||||
>
|
||||
.{HOSTED_COMMUNITY_SUFFIX}
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex w-full max-w-[640px] flex-col items-center text-center">
|
||||
<h1 className="text-title font-normal">Your communities</h1>
|
||||
<p className="mx-auto mt-3 max-w-[480px] text-sm leading-6 text-foreground/80">
|
||||
Sign in to connect a community you already own on this machine, or
|
||||
create a new one.
|
||||
<div className="flex min-h-[calc(100dvh-15.625rem)] w-full max-w-[920px] flex-col items-center text-center">
|
||||
<h1 className="max-w-[620px] text-title font-normal leading-[1.18] tracking-[-0.025em]">
|
||||
{hasCommunities ? "Choose a community" : "Create a community"}
|
||||
</h1>
|
||||
<p className="mx-auto mt-2 max-w-[560px] text-sm leading-6 text-foreground">
|
||||
{hasCommunities
|
||||
? "Connect one you own, or start something new."
|
||||
: "Claim a Buzz address to get started."}
|
||||
</p>
|
||||
|
||||
<div className="mt-10 w-full space-y-5 text-left">
|
||||
{error ? (
|
||||
<div
|
||||
className="flex items-start gap-2 rounded-xl border border-destructive/40 bg-destructive/5 p-4 text-sm text-destructive"
|
||||
role="alert"
|
||||
>
|
||||
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="flex w-full flex-1 flex-col justify-center text-left">
|
||||
{loading ? (
|
||||
<div className="flex justify-center py-10" role="status">
|
||||
<LoaderCircle className="h-6 w-6 animate-spin" />
|
||||
<span className="sr-only">Checking Builderlab sign-in</span>
|
||||
<span className="sr-only">Checking sign-in</span>
|
||||
</div>
|
||||
) : !auth ? (
|
||||
<section className="rounded-xl bg-white/75 p-6 text-center">
|
||||
<h2 className="font-medium">Sign in or create an account</h2>
|
||||
<p className="mt-2 text-sm text-foreground/70">
|
||||
Builderlab provides Block-hosted Buzz communities. Authentication
|
||||
opens in your browser and returns here.
|
||||
</p>
|
||||
{action === "Signing in…" ? (
|
||||
<div className="mt-5 flex flex-col items-center gap-3">
|
||||
<div className="flex items-center gap-2 text-sm text-foreground/70">
|
||||
<LoaderCircle className="h-4 w-4 animate-spin" />
|
||||
Waiting for your browser…
|
||||
</div>
|
||||
<Button onClick={cancelSignIn} variant="outline">
|
||||
Cancel sign-in
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button className="mt-5 rounded-full px-6" onClick={signIn}>
|
||||
Continue with Builderlab
|
||||
</Button>
|
||||
)}
|
||||
</section>
|
||||
) : !identity ? (
|
||||
<section className="rounded-xl bg-white/75 p-6 text-center">
|
||||
<h2 className="font-medium">Connect this Buzz identity</h2>
|
||||
<p className="mt-2 text-sm text-foreground/70">
|
||||
Link this device’s Buzz key to{" "}
|
||||
{auth.email ?? auth.name ?? "your Builderlab account"}. Buzz signs
|
||||
a one-time challenge locally; your private key never leaves
|
||||
Desktop.
|
||||
</p>
|
||||
<Button
|
||||
className="mt-5 rounded-full px-6"
|
||||
disabled={busy}
|
||||
onClick={() => void connectIdentity()}
|
||||
>
|
||||
{busy ? <LoaderCircle className="h-4 w-4 animate-spin" /> : null}
|
||||
{action ?? "Connect this Buzz identity"}
|
||||
</Button>
|
||||
</section>
|
||||
) : identityMismatch ? (
|
||||
<section className="rounded-xl border border-amber-500/50 bg-amber-500/5 p-6">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0 text-amber-600" />
|
||||
<div>
|
||||
<h2 className="font-medium">
|
||||
This account uses a different Buzz identity
|
||||
</h2>
|
||||
<p className="mt-2 text-sm text-foreground/70">
|
||||
This Builderlab account is connected to another Buzz identity.
|
||||
You can disconnect that identity and reconnect this device, or
|
||||
sign out to use a different email.
|
||||
) : ready ? (
|
||||
<>
|
||||
{errorBox}
|
||||
{hasCommunities ? (
|
||||
<>
|
||||
<Card
|
||||
className={`${FUZZY_SURFACE_CLASS} !max-w-[760px]`}
|
||||
data-testid="hosted-community-list-surface"
|
||||
variant="textured"
|
||||
>
|
||||
<section className={COMMUNITY_LIST_CLASS}>
|
||||
<h2 className="text-center text-sm font-medium">
|
||||
Your communities
|
||||
</h2>
|
||||
<ul className="mt-2">
|
||||
{activeCommunities.map((community, index) => (
|
||||
<li
|
||||
className={`${COMMUNITY_ROW_CLASS} ${COMMUNITY_DIVIDER_CLASS}`}
|
||||
key={
|
||||
community.id ?? community.normalized_host ?? index
|
||||
}
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm">
|
||||
{community.name ??
|
||||
community.slug ??
|
||||
"Hosted community"}
|
||||
</p>
|
||||
<p className="mt-1 truncate text-sm text-foreground/55">
|
||||
{community.normalized_host}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
className={COMMUNITY_ACTION_CLASS}
|
||||
disabled={busy}
|
||||
onClick={() =>
|
||||
void run("Connecting community…", async () => {
|
||||
connect(community);
|
||||
})
|
||||
}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
>
|
||||
Connect
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<AnimatePresence initial={false} mode="wait">
|
||||
{!showCreate ? (
|
||||
<motion.div
|
||||
animate={{ opacity: 1, transform: "translateX(0px)" }}
|
||||
className={COMMUNITY_ROW_CLASS}
|
||||
exit={
|
||||
shouldReduceMotion
|
||||
? { opacity: 0 }
|
||||
: {
|
||||
opacity: 0,
|
||||
transform: "translateX(-12px)",
|
||||
}
|
||||
}
|
||||
initial={false}
|
||||
key="add-community-action"
|
||||
transition={{
|
||||
duration: shouldReduceMotion ? 0 : 0.18,
|
||||
ease: "easeOut",
|
||||
}}
|
||||
>
|
||||
<p className="text-sm">
|
||||
Want to create a new community?
|
||||
</p>
|
||||
<Button
|
||||
className={COMMUNITY_ACTION_CLASS}
|
||||
disabled={busy || atCommunityLimit}
|
||||
onClick={() => setShowCreate(true)}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
+ Add new
|
||||
</Button>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
animate={{ opacity: 1, transform: "translateX(0px)" }}
|
||||
initial={
|
||||
shouldReduceMotion
|
||||
? { opacity: 1 }
|
||||
: {
|
||||
opacity: 0,
|
||||
transform: "translateX(12px)",
|
||||
}
|
||||
}
|
||||
key="add-community-input"
|
||||
transition={{
|
||||
duration: shouldReduceMotion ? 0 : 0.22,
|
||||
ease: "easeOut",
|
||||
}}
|
||||
>
|
||||
{renderCreationForm(true)}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</section>
|
||||
</Card>
|
||||
<p
|
||||
aria-live="polite"
|
||||
className={`relative z-10 mt-3 min-h-5 text-center text-sm ${
|
||||
creationFeedback ? "visible" : "invisible"
|
||||
} ${
|
||||
availability === false || (name && !validName)
|
||||
? "text-destructive"
|
||||
: "text-[color:var(--buzz-onboarding-backup-ink)]"
|
||||
}`}
|
||||
id="hosted-community-feedback"
|
||||
>
|
||||
{creationFeedback ?? "Community address status"}
|
||||
</p>
|
||||
<p className="mt-3 break-all font-mono text-xs text-foreground/60">
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{renderCreationForm(false)}
|
||||
<p
|
||||
aria-live="polite"
|
||||
className={`relative z-10 mt-3 min-h-5 text-center text-sm ${
|
||||
creationFeedback ? "visible" : "invisible"
|
||||
} ${
|
||||
availability === false || (name && !validName)
|
||||
? "text-destructive"
|
||||
: "text-[color:var(--buzz-onboarding-backup-ink)]"
|
||||
}`}
|
||||
id="hosted-community-feedback"
|
||||
>
|
||||
{creationFeedback ?? "Community address status"}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Card
|
||||
aria-hidden
|
||||
className={`${FUZZY_SURFACE_CLASS} opacity-70`}
|
||||
variant="textured"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!modalOpen ? (
|
||||
<OnboardingFooter>
|
||||
<Button
|
||||
className={PAGE_CTA_CLASS}
|
||||
disabled={
|
||||
!validName ||
|
||||
availability === false ||
|
||||
checkingName ||
|
||||
busy ||
|
||||
atCommunityLimit
|
||||
}
|
||||
form="hosted-community-create-form"
|
||||
type="submit"
|
||||
>
|
||||
{busy ? <LoaderCircle className="h-4 w-4 animate-spin" /> : null}
|
||||
{action ?? "Next"}
|
||||
</Button>
|
||||
<Button
|
||||
className={PAGE_BACK_CLASS}
|
||||
disabled={busy}
|
||||
onClick={goBack}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
</OnboardingFooter>
|
||||
) : null}
|
||||
|
||||
<Dialog
|
||||
open={modalOpen}
|
||||
onOpenChange={(open) => {
|
||||
if (open) return;
|
||||
if (action === "Signing in…") {
|
||||
cancelSignInAndGoBack();
|
||||
return;
|
||||
}
|
||||
if (!busy) goBack();
|
||||
}}
|
||||
>
|
||||
<DialogContent
|
||||
className="buzz-onboarding-neutral-theme max-w-[560px] text-foreground [&_button]:shadow-none"
|
||||
closeButtonClassName={ONBOARDING_INK_ICON_CLASS}
|
||||
data-system-color-scheme="light"
|
||||
overlayClassName="bg-[rgb(var(--buzz-hosted-community-modal-overlay-bg)/0.25)]"
|
||||
surface="textured"
|
||||
>
|
||||
<div className="mx-auto flex w-full max-w-sm flex-col items-center py-2 text-center">
|
||||
<BuzzMark className="mb-5 h-auto w-9 text-foreground" />
|
||||
|
||||
{!auth ? (
|
||||
<>
|
||||
<DialogTitle className="text-xl font-medium text-foreground">
|
||||
Set up your community
|
||||
</DialogTitle>
|
||||
<DialogDescription className="mt-2 text-sm leading-6 text-foreground">
|
||||
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.
|
||||
</DialogDescription>
|
||||
{errorBox ? (
|
||||
<div className="mt-5 w-full">{errorBox}</div>
|
||||
) : null}
|
||||
{action === "Signing in…" ? (
|
||||
<Button
|
||||
className={`mt-6 ${MODAL_PRIMARY_ACTION_CLASS}`}
|
||||
disabled
|
||||
>
|
||||
<LoaderCircle className="h-4 w-4 animate-spin" />
|
||||
Waiting for your browser…
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className={`mt-6 ${MODAL_PRIMARY_ACTION_CLASS}`}
|
||||
onClick={signIn}
|
||||
>
|
||||
Sign in to continue
|
||||
</Button>
|
||||
)}
|
||||
{/* Quiet breadcrumb: Buzz itself is open source; this hosted
|
||||
relay is the one account-backed piece of the flow. */}
|
||||
<p className="mt-6 w-full border-t border-foreground/10 pt-4 text-xs leading-5 text-foreground/45">
|
||||
Buzz is open source. Builderlab hosts the relay for this
|
||||
account.
|
||||
</p>
|
||||
</>
|
||||
) : !identity ? (
|
||||
<>
|
||||
<DialogTitle className="text-xl font-medium text-foreground">
|
||||
Finish connecting Buzz
|
||||
</DialogTitle>
|
||||
<DialogDescription className="mt-2 text-sm leading-6 text-foreground">
|
||||
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.
|
||||
</DialogDescription>
|
||||
{errorBox ? (
|
||||
<div className="mt-5 w-full">{errorBox}</div>
|
||||
) : null}
|
||||
<Button
|
||||
className={`mt-6 ${MODAL_PRIMARY_ACTION_CLASS}`}
|
||||
disabled={busy}
|
||||
onClick={() => void connectIdentity()}
|
||||
>
|
||||
{busy ? (
|
||||
<LoaderCircle className="h-4 w-4 animate-spin" />
|
||||
) : null}
|
||||
{busy ? action : "Connect and continue"}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<DialogTitle className="text-xl font-medium text-foreground">
|
||||
This account uses a different Buzz identity
|
||||
</DialogTitle>
|
||||
<DialogDescription className="mt-2 text-sm leading-6 text-foreground">
|
||||
This account is connected to another Buzz identity. Reconnect
|
||||
this device, or sign out to use a different email.
|
||||
</DialogDescription>
|
||||
<p className="mt-4 w-full break-all rounded-xl bg-[rgb(var(--buzz-hosted-community-identity-bg)/0.5)] px-4 py-3 text-left font-mono text-xs text-foreground">
|
||||
Account: {identity.npub ?? boundPubkey}
|
||||
<br />
|
||||
This device: {localNpub ?? localPubkey}
|
||||
</p>
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{errorBox ? (
|
||||
<div className="mt-5 w-full">{errorBox}</div>
|
||||
) : null}
|
||||
<div className="mt-6 flex flex-col items-stretch gap-2">
|
||||
<Button
|
||||
className={MODAL_PRIMARY_ACTION_CLASS}
|
||||
disabled={busy}
|
||||
onClick={() => void switchToDeviceIdentity()}
|
||||
>
|
||||
{action ?? "Use this device's identity"}
|
||||
{busy ? action : "Use this device's identity"}
|
||||
</Button>
|
||||
<Button
|
||||
className={MODAL_BACK_ACTION_CLASS}
|
||||
disabled={busy}
|
||||
onClick={() => void signOut()}
|
||||
variant="outline"
|
||||
variant="ghost"
|
||||
>
|
||||
Sign in with a different email
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-center gap-2 rounded-xl bg-white/60 px-4 py-3 text-sm text-foreground/70">
|
||||
<CheckCircle2 className="h-4 w-4 text-emerald-600" />
|
||||
Signed in{auth.email ? ` as ${auth.email}` : ""} with this Buzz
|
||||
identity
|
||||
</div>
|
||||
|
||||
{activeCommunities.length > 0 ? (
|
||||
<section className="rounded-xl bg-white/75 p-5">
|
||||
<h2 className="font-medium">Connect to one you own</h2>
|
||||
<ul className="mt-3 space-y-2">
|
||||
{activeCommunities.map((community, index) => (
|
||||
<li
|
||||
className="flex items-center justify-between gap-4 rounded-lg border border-foreground/10 bg-white/50 p-3"
|
||||
key={community.id ?? community.normalized_host ?? index}
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-medium">
|
||||
{community.name ??
|
||||
community.slug ??
|
||||
"Hosted community"}
|
||||
</p>
|
||||
<p className="truncate text-xs text-foreground/60">
|
||||
{community.normalized_host}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
className="rounded-full"
|
||||
disabled={busy}
|
||||
onClick={() =>
|
||||
void run("Connecting community…", async () => {
|
||||
connect(community);
|
||||
})
|
||||
}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
Connect
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<form className="rounded-xl bg-white/75 p-5" onSubmit={create}>
|
||||
<h2 className="font-medium">Create a new community</h2>
|
||||
<p className="mt-1 text-sm text-foreground/70">
|
||||
Choose the address your team will use to connect.
|
||||
</p>
|
||||
<div className="mt-4 flex items-center gap-2">
|
||||
<Input
|
||||
aria-label="Community address"
|
||||
autoComplete="off"
|
||||
disabled={busy || atCommunityLimit}
|
||||
maxLength={63}
|
||||
onChange={(event) => {
|
||||
setName(event.target.value.toLowerCase());
|
||||
setAvailability(null);
|
||||
}}
|
||||
placeholder="north-star"
|
||||
spellCheck={false}
|
||||
value={name}
|
||||
/>
|
||||
<span className="shrink-0 text-sm text-foreground/60">
|
||||
.{HOSTED_COMMUNITY_SUFFIX}
|
||||
</span>
|
||||
</div>
|
||||
{atCommunityLimit ? (
|
||||
<p className="mt-2 text-sm text-foreground/60">
|
||||
You’ve reached the limit of {HOSTED_COMMUNITY_LIMIT} hosted
|
||||
communities.
|
||||
</p>
|
||||
) : name && !validName ? (
|
||||
<p className="mt-2 text-sm text-destructive">
|
||||
Use lowercase letters, numbers, and single hyphens.
|
||||
</p>
|
||||
) : checkingName ? (
|
||||
<p className="mt-2 text-sm text-foreground/60">
|
||||
Checking availability…
|
||||
</p>
|
||||
) : availability === false ? (
|
||||
<p className="mt-2 text-sm text-destructive">
|
||||
That address is already taken.
|
||||
</p>
|
||||
) : availability === true ? (
|
||||
<p className="mt-2 text-sm text-emerald-600">
|
||||
That address is available.
|
||||
</p>
|
||||
) : null}
|
||||
<Button
|
||||
className="mt-4 rounded-full px-6"
|
||||
disabled={
|
||||
!validName ||
|
||||
availability === false ||
|
||||
checkingName ||
|
||||
busy ||
|
||||
atCommunityLimit
|
||||
}
|
||||
type="submit"
|
||||
>
|
||||
{busy ? (
|
||||
<LoaderCircle className="h-4 w-4 animate-spin" />
|
||||
) : null}
|
||||
{action ?? "Create and connect"}
|
||||
</Button>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<OnboardingFooter>
|
||||
<Button
|
||||
className="h-9 rounded-full bg-foreground/10 px-6 hover:bg-foreground/15"
|
||||
disabled={busy}
|
||||
onClick={goBack}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
</OnboardingFooter>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@ export function WelcomeSetup({
|
||||
setRelayUrlError(null);
|
||||
communityOnboarding.start({
|
||||
source: "first-community",
|
||||
firstCommunityPage: "join",
|
||||
relayUrl: normalizedRelayUrl,
|
||||
});
|
||||
},
|
||||
@@ -108,6 +109,7 @@ export function WelcomeSetup({
|
||||
(relayWsUrl: string, code: string, policyReceipt?: string) => {
|
||||
communityOnboarding.start({
|
||||
source: "first-community",
|
||||
firstCommunityPage: "join",
|
||||
relayUrl: relayWsUrl,
|
||||
inviteCode: code,
|
||||
policyReceipt,
|
||||
|
||||
@@ -26,9 +26,13 @@ export type CommunityOnboardingStage =
|
||||
*/
|
||||
| "entering";
|
||||
|
||||
export type FirstCommunityPage = "join" | "owned";
|
||||
|
||||
export type CommunityOnboardingTransaction = {
|
||||
id: string;
|
||||
source: CommunityOnboardingSource;
|
||||
/** First-run screen that launched this transaction, restored on cancel. */
|
||||
firstCommunityPage?: FirstCommunityPage;
|
||||
stage: CommunityOnboardingStage;
|
||||
relayUrl: string;
|
||||
inviteCode?: string;
|
||||
@@ -68,6 +72,7 @@ export type CommunityOnboardingTransactionPatch = Partial<
|
||||
|
||||
export type StartCommunityOnboardingInput = {
|
||||
source: CommunityOnboardingSource;
|
||||
firstCommunityPage?: FirstCommunityPage;
|
||||
relayUrl: string;
|
||||
inviteCode?: string;
|
||||
communityName?: string;
|
||||
@@ -150,6 +155,8 @@ export function startCommunityOnboarding(
|
||||
if (existing?.relayUrl === relayUrl) {
|
||||
const updated = {
|
||||
...existing,
|
||||
firstCommunityPage:
|
||||
input.firstCommunityPage ?? existing.firstCommunityPage,
|
||||
inviteCode: input.inviteCode?.trim() || existing.inviteCode,
|
||||
communityName: input.communityName?.trim() || existing.communityName,
|
||||
token: input.token?.trim() || existing.token,
|
||||
@@ -169,6 +176,7 @@ export function startCommunityOnboarding(
|
||||
const transaction: CommunityOnboardingTransaction = {
|
||||
id: crypto.randomUUID(),
|
||||
source: input.source,
|
||||
firstCommunityPage: input.firstCommunityPage,
|
||||
stage: input.inviteCode?.trim() ? "claiming" : "connecting",
|
||||
relayUrl,
|
||||
inviteCode: input.inviteCode?.trim() || undefined,
|
||||
|
||||
@@ -53,6 +53,16 @@
|
||||
--buzz-gradient-dark-top: #4a4616;
|
||||
--buzz-gradient-dark-bottom: #0a1423;
|
||||
--buzz-content-dark: 0 0% 10.1960784314%;
|
||||
/* Hosted-community onboarding intentionally keeps this light palette in
|
||||
both app themes; semantic tokens keep component code color-agnostic. */
|
||||
--buzz-hosted-community-surface-fg: 23 23 23;
|
||||
--buzz-hosted-community-divider-border: 150 150 10;
|
||||
--buzz-hosted-community-action-bg: 240 240 205;
|
||||
--buzz-hosted-community-action-bg-hover: 232 232 188;
|
||||
--buzz-hosted-community-input-bg: 255 255 255;
|
||||
--buzz-hosted-community-modal-action-fg: 255 255 255;
|
||||
--buzz-hosted-community-modal-overlay-bg: 0 0 0;
|
||||
--buzz-hosted-community-identity-bg: 255 255 255;
|
||||
}
|
||||
|
||||
.dark {
|
||||
|
||||
@@ -45,6 +45,8 @@ type DialogContentProps = React.ComponentPropsWithoutRef<
|
||||
> & {
|
||||
/** Extra classes for the built-in close button (e.g. a themed icon color). */
|
||||
closeButtonClassName?: string;
|
||||
/** Extra classes for this dialog's backdrop. */
|
||||
overlayClassName?: string;
|
||||
overlayVariant?: "default" | "transparent";
|
||||
showCloseButton?: boolean;
|
||||
/**
|
||||
@@ -66,6 +68,7 @@ const DialogContent = React.forwardRef<
|
||||
className,
|
||||
children,
|
||||
closeButtonClassName,
|
||||
overlayClassName,
|
||||
overlayVariant = "default",
|
||||
showCloseButton = true,
|
||||
surface = "default",
|
||||
@@ -76,11 +79,12 @@ const DialogContent = React.forwardRef<
|
||||
<DialogPortal>
|
||||
<DialogOverlay
|
||||
data-testid="dialog-overlay"
|
||||
className={
|
||||
className={cn(
|
||||
overlayVariant === "transparent"
|
||||
? "bg-transparent backdrop-blur-none"
|
||||
: undefined
|
||||
}
|
||||
: undefined,
|
||||
overlayClassName,
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
@@ -616,13 +616,15 @@ test("first-community choices expose npub and invite input", async ({
|
||||
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: "Your communities" }),
|
||||
page.getByRole("heading", { name: "Set up your community" }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Continue with Builderlab" }),
|
||||
page.getByRole("button", { name: "Sign in to continue" }),
|
||||
).toBeVisible();
|
||||
await page.getByRole("button", { name: "Back" }).click();
|
||||
// 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(
|
||||
@@ -795,6 +797,21 @@ test("first-community owner can connect an existing hosted community", async ({
|
||||
),
|
||||
)
|
||||
.toContain("wss://north-star.communities.buzz.xyz");
|
||||
await page.getByTestId("community-profile-back").click();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Choose a community" }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText("North Star")).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Request access to community" }),
|
||||
).toHaveCount(0);
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(() =>
|
||||
window.localStorage.getItem("buzz-community-onboarding-transaction.v1"),
|
||||
),
|
||||
)
|
||||
.toBeNull();
|
||||
});
|
||||
|
||||
test("first-community owner can create and connect a hosted community", async ({
|
||||
@@ -821,19 +838,46 @@ test("first-community owner can create and connect a hosted community", async ({
|
||||
await page
|
||||
.getByRole("button", { name: "Create or connect to my own community" })
|
||||
.click();
|
||||
await page.getByRole("button", { name: "Continue with Builderlab" }).click();
|
||||
await page.getByRole("button", { name: "Sign in to continue" }).click();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Connect this Buzz identity" }),
|
||||
page.getByRole("heading", { name: "Finish connecting Buzz" }),
|
||||
).toBeVisible();
|
||||
await page
|
||||
.getByRole("button", { name: "Connect this Buzz identity" })
|
||||
.click();
|
||||
await expect(page.getByText("Signed in as owner@example.com")).toBeVisible();
|
||||
await page
|
||||
.getByRole("textbox", { name: "Community address" })
|
||||
.fill("bee-lab");
|
||||
await expect(page.getByText("That address is available.")).toBeVisible();
|
||||
await page.getByRole("button", { name: "Create and connect" }).click();
|
||||
await page.getByRole("button", { name: "Connect and continue" }).click();
|
||||
const createSurface = page.getByTestId("hosted-community-create-surface");
|
||||
const surfaceBoxBeforeFeedback = await createSurface.boundingBox();
|
||||
const communityNameInput = page.getByTestId("hosted-community-address-input");
|
||||
await communityNameInput.fill("bee-lab");
|
||||
await expect(communityNameInput).toHaveAttribute("style", /width: 7ch;/);
|
||||
const availabilityFeedback = page.getByText("That address is available.");
|
||||
await expect(availabilityFeedback).toBeVisible();
|
||||
const [feedbackBox, surfaceBox, inputBox, suffixBox] = await Promise.all([
|
||||
availabilityFeedback.boundingBox(),
|
||||
createSurface.boundingBox(),
|
||||
page.getByTestId("hosted-community-address-input").boundingBox(),
|
||||
page.locator("#hosted-community-suffix").boundingBox(),
|
||||
]);
|
||||
if (
|
||||
!surfaceBoxBeforeFeedback ||
|
||||
!feedbackBox ||
|
||||
!surfaceBox ||
|
||||
!inputBox ||
|
||||
!suffixBox
|
||||
) {
|
||||
throw new Error("Could not measure hosted community creation layout");
|
||||
}
|
||||
expect(surfaceBox.y).toBe(surfaceBoxBeforeFeedback.y);
|
||||
expect(surfaceBox.height).toBe(surfaceBoxBeforeFeedback.height);
|
||||
const addressLeft = inputBox.x;
|
||||
const addressRight = suffixBox.x + suffixBox.width;
|
||||
expect(
|
||||
Math.abs(
|
||||
(addressLeft + addressRight) / 2 - (surfaceBox.x + surfaceBox.width / 2),
|
||||
),
|
||||
).toBeLessThanOrEqual(1);
|
||||
expect(feedbackBox.y).toBeGreaterThanOrEqual(
|
||||
surfaceBox.y + surfaceBox.height,
|
||||
);
|
||||
await page.getByRole("button", { name: "Next" }).click();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Build your profile" }),
|
||||
).toBeVisible();
|
||||
@@ -846,6 +890,69 @@ test("first-community owner can create and connect a hosted community", async ({
|
||||
.toContain("wss://bee-lab.communities.buzz.xyz");
|
||||
});
|
||||
|
||||
test("hosted community address line stays within the card for a long name", async ({
|
||||
page,
|
||||
}) => {
|
||||
await seedActiveIdentity(page, BLANK_TYLER_IDENTITY);
|
||||
await page.addInitScript((pubkey) => {
|
||||
window.localStorage.setItem(
|
||||
`buzz-machine-onboarding-complete.v2:${pubkey}`,
|
||||
"true",
|
||||
);
|
||||
}, BLANK_TYLER_IDENTITY.pubkey);
|
||||
await installMockBridge(
|
||||
page,
|
||||
{},
|
||||
{
|
||||
relayWsUrl: "wss://default.example.com",
|
||||
skipOnboardingSeed: true,
|
||||
skipCommunitySeed: true,
|
||||
},
|
||||
);
|
||||
// The 800px app minimum is the worst case for the full-width address line.
|
||||
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.getByRole("button", { name: "Sign in to continue" }).click();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Finish connecting Buzz" }),
|
||||
).toBeVisible();
|
||||
await page.getByRole("button", { name: "Connect and continue" }).click();
|
||||
|
||||
const createSurface = page.getByTestId("hosted-community-create-surface");
|
||||
const communityNameInput = page.getByTestId("hosted-community-address-input");
|
||||
// A maximum-length (63 char) valid name — the overflow case Wes flagged; the
|
||||
// 7-char check above cannot catch it.
|
||||
const longName = "a".repeat(63);
|
||||
await communityNameInput.fill(longName);
|
||||
await expect(communityNameInput).toHaveValue(longName);
|
||||
|
||||
const [surfaceBox, inputBox, suffixBox] = await Promise.all([
|
||||
createSurface.boundingBox(),
|
||||
communityNameInput.boundingBox(),
|
||||
page.locator("#hosted-community-suffix").boundingBox(),
|
||||
]);
|
||||
if (!surfaceBox || !inputBox || !suffixBox) {
|
||||
throw new Error("Could not measure hosted community creation layout");
|
||||
}
|
||||
const addressLeft = inputBox.x;
|
||||
const addressRight = suffixBox.x + suffixBox.width;
|
||||
// The composed `<name>.<suffix>` line must stay within the card — no
|
||||
// horizontal overflow past the surface or the 800px window.
|
||||
expect(addressLeft).toBeGreaterThanOrEqual(surfaceBox.x);
|
||||
expect(addressRight).toBeLessThanOrEqual(surfaceBox.x + surfaceBox.width);
|
||||
expect(addressRight).toBeLessThanOrEqual(800);
|
||||
// …and it stays centered within the card.
|
||||
expect(
|
||||
Math.abs(
|
||||
(addressLeft + addressRight) / 2 - (surfaceBox.x + surfaceBox.width / 2),
|
||||
),
|
||||
).toBeLessThanOrEqual(2);
|
||||
});
|
||||
|
||||
test("first-community reports a created community without a relay address", async ({
|
||||
page,
|
||||
}) => {
|
||||
@@ -880,11 +987,9 @@ test("first-community reports a created community without a relay address", asyn
|
||||
await page
|
||||
.getByRole("button", { name: "Create or connect to my own community" })
|
||||
.click();
|
||||
await page
|
||||
.getByRole("textbox", { name: "Community address" })
|
||||
.fill("bee-lab");
|
||||
await page.getByRole("textbox", { name: "Community name" }).fill("bee-lab");
|
||||
await expect(page.getByText("That address is available.")).toBeVisible();
|
||||
await page.getByRole("button", { name: "Create and connect" }).click();
|
||||
await page.getByRole("button", { name: "Next" }).click();
|
||||
await expect(page.getByRole("alert")).toContainText(
|
||||
"The community was created, but Builderlab did not return its relay address.",
|
||||
);
|
||||
@@ -893,9 +998,7 @@ test("first-community reports a created community without a relay address", asyn
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("first-community can cancel a closed-browser sign-in and retry", async ({
|
||||
page,
|
||||
}) => {
|
||||
test("first-community X cancels a pending sign-in", async ({ page }) => {
|
||||
await seedActiveIdentity(page, BLANK_TYLER_IDENTITY);
|
||||
await page.addInitScript((pubkey) => {
|
||||
window.localStorage.setItem(
|
||||
@@ -917,24 +1020,18 @@ test("first-community can cancel a closed-browser sign-in and retry", async ({
|
||||
await page
|
||||
.getByRole("button", { name: "Create or connect to my own community" })
|
||||
.click();
|
||||
await page.getByRole("button", { name: "Continue with Builderlab" }).click();
|
||||
await page.getByRole("button", { name: "Sign in to continue" }).click();
|
||||
await expect(page.getByText("Waiting for your browser…")).toBeVisible();
|
||||
await page.getByRole("button", { name: "Cancel sign-in" }).click();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Continue with Builderlab" }),
|
||||
page.getByRole("button", { name: "Cancel sign-in" }),
|
||||
).toHaveCount(0);
|
||||
await page.getByRole("button", { name: "Close" }).click();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Create or connect to my own community" }),
|
||||
).toBeVisible();
|
||||
await expect
|
||||
.poll(() => page.evaluate(() => window.__BUZZ_E2E_COMMANDS__ ?? []))
|
||||
.toEqual(expect.arrayContaining(["cancel_builderlab_login"]));
|
||||
|
||||
await page.evaluate(() => {
|
||||
if (window.__BUZZ_E2E_CONFIG__?.mock)
|
||||
window.__BUZZ_E2E_CONFIG__.mock.builderlabLoginDelayMs = 0;
|
||||
});
|
||||
await page.getByRole("button", { name: "Continue with Builderlab" }).click();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Connect this Buzz identity" }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("first-community owner can replace a mismatched account identity", async ({
|
||||
@@ -976,7 +1073,7 @@ test("first-community owner can replace a mismatched account identity", async ({
|
||||
.getByRole("button", { name: "Use this device's identity" })
|
||||
.click();
|
||||
await expect(
|
||||
page.getByText("Signed in as old-owner@example.com"),
|
||||
page.getByRole("textbox", { name: "Community name" }),
|
||||
).toBeVisible();
|
||||
await expect
|
||||
.poll(() => page.evaluate(() => window.__BUZZ_E2E_COMMANDS__ ?? []))
|
||||
@@ -1028,7 +1125,7 @@ test("first-community explains when the local identity belongs to another accoun
|
||||
),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Connect this Buzz identity" }),
|
||||
page.getByRole("heading", { name: "Finish connecting Buzz" }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -1066,9 +1163,7 @@ test("back clears Builderlab auth before returning to first-community choices",
|
||||
await page
|
||||
.getByRole("button", { name: "Create or connect to my own community" })
|
||||
.click();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Continue with Builderlab" }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Continue" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("first-community shows the scenario cards for localhost", async ({
|
||||
|
||||
Reference in New Issue
Block a user