mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Simplify first-community onboarding choices (UI only) (#2194)
This commit is contained in:
@@ -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 ? (
|
||||
<div
|
||||
className="flex items-start gap-3 rounded-xl border border-destructive/30 bg-destructive/10 p-4 text-left"
|
||||
@@ -426,6 +454,130 @@ export function HostedCommunityOnboarding({ onBack }: { onBack: () => void }) {
|
||||
</Card>
|
||||
);
|
||||
|
||||
const signInDialog = (
|
||||
<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>
|
||||
{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()}
|
||||
>
|
||||
{busy ? action : "Use this device's identity"}
|
||||
</Button>
|
||||
<Button
|
||||
className={MODAL_BACK_ACTION_CLASS}
|
||||
disabled={busy}
|
||||
onClick={() => void signOut()}
|
||||
variant="ghost"
|
||||
>
|
||||
Sign in with a different email
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
// 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 (
|
||||
<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]">
|
||||
@@ -619,127 +771,7 @@ export function HostedCommunityOnboarding({ onBack }: { onBack: () => void }) {
|
||||
</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>
|
||||
{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()}
|
||||
>
|
||||
{busy ? action : "Use this device's identity"}
|
||||
</Button>
|
||||
<Button
|
||||
className={MODAL_BACK_ACTION_CLASS}
|
||||
disabled={busy}
|
||||
onClick={() => void signOut()}
|
||||
variant="ghost"
|
||||
>
|
||||
Sign in with a different email
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{signInDialog}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
ONBOARDING_KEY_ROW_CLASS,
|
||||
ONBOARDING_KEY_TEXT_CLASS,
|
||||
} from "@/features/onboarding/ui/NsecMaskedDisplay";
|
||||
import {
|
||||
type OnboardingTransitionDirection,
|
||||
OnboardingSlideTransition,
|
||||
} from "@/features/onboarding/ui/OnboardingSlideTransition";
|
||||
import { OnboardingChrome } from "@/features/onboarding/ui/OnboardingChrome";
|
||||
import {
|
||||
OnboardingFooter,
|
||||
OnboardingFooterProvider,
|
||||
} from "@/features/onboarding/ui/OnboardingFooter";
|
||||
import { getIdentity } from "@/shared/api/tauriIdentity";
|
||||
import { pubkeyToNpub } from "@/shared/lib/nostrUtils";
|
||||
import {
|
||||
type OnboardingTransitionDirection,
|
||||
OnboardingSlideTransition,
|
||||
} from "@/features/onboarding/ui/OnboardingSlideTransition";
|
||||
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<WelcomeSetupPage>(initialPage);
|
||||
const [transitionMode, setTransitionMode] =
|
||||
React.useState<WelcomeTransitionMode>(initialTransitionMode);
|
||||
const [npub, setNpub] = React.useState("");
|
||||
const [identityError, setIdentityError] = React.useState<string | null>(null);
|
||||
const [relayUrl, setRelayUrl] = React.useState("");
|
||||
const [relayUrlError, setRelayUrlError] = React.useState<string | null>(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;
|
||||
}
|
||||
|
||||
setRelayUrlError(null);
|
||||
communityOnboarding.start({
|
||||
source: "first-community",
|
||||
firstCommunityPage: "join",
|
||||
relayUrl: normalizedRelayUrl,
|
||||
});
|
||||
setPage(nextPage);
|
||||
},
|
||||
[communityOnboarding, relayUrl],
|
||||
[],
|
||||
);
|
||||
|
||||
const handleInviteRedeem = React.useCallback(
|
||||
(relayWsUrl: string, code: string, policyReceipt?: string) => {
|
||||
const startConnection = React.useCallback(
|
||||
(relayUrl: string) => {
|
||||
communityOnboarding.start({
|
||||
source: "first-community",
|
||||
firstCommunityPage: "join",
|
||||
relayUrl: relayWsUrl,
|
||||
firstCommunityPage: page === "member" ? "member" : "join",
|
||||
relayUrl,
|
||||
});
|
||||
},
|
||||
[communityOnboarding, page],
|
||||
);
|
||||
|
||||
const redeemInvite = React.useCallback(
|
||||
(relayUrl: string, code: string, policyReceipt?: string) => {
|
||||
communityOnboarding.start({
|
||||
source: "first-community",
|
||||
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
|
||||
</h1>
|
||||
<p className="mt-3 text-sm leading-6 text-foreground/80">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex w-full flex-1 translate-y-16 flex-col items-center justify-center gap-20 py-8">
|
||||
@@ -155,8 +115,12 @@ export function WelcomeSetup({
|
||||
className={COMMUNITY_OPTION_CARD_CLASS}
|
||||
variant="textured"
|
||||
>
|
||||
<button onClick={() => showPage("join")} type="button">
|
||||
Add me to a community
|
||||
<button
|
||||
data-testid="community-choice-join"
|
||||
onClick={() => showPage("join")}
|
||||
type="button"
|
||||
>
|
||||
Join a community
|
||||
</button>
|
||||
</Card>
|
||||
<Card
|
||||
@@ -164,8 +128,12 @@ export function WelcomeSetup({
|
||||
className={COMMUNITY_OPTION_CARD_CLASS}
|
||||
variant="textured"
|
||||
>
|
||||
<button onClick={() => showPage("invite")} type="button">
|
||||
I have an invite link
|
||||
<button
|
||||
data-testid="community-choice-create"
|
||||
onClick={() => setIsHostedSignInOpen(true)}
|
||||
type="button"
|
||||
>
|
||||
Create a community
|
||||
</button>
|
||||
</Card>
|
||||
<Card
|
||||
@@ -173,10 +141,12 @@ export function WelcomeSetup({
|
||||
className={COMMUNITY_OPTION_CARD_CLASS}
|
||||
variant="textured"
|
||||
>
|
||||
<button onClick={() => showPage("owned")} type="button">
|
||||
<span className="max-w-52">
|
||||
Create or connect to my own community
|
||||
</span>
|
||||
<button
|
||||
data-testid="community-choice-existing"
|
||||
onClick={() => showPage("existing")}
|
||||
type="button"
|
||||
>
|
||||
I already have a community
|
||||
</button>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -192,146 +162,53 @@ export function WelcomeSetup({
|
||||
</Button>
|
||||
</OnboardingFooter>
|
||||
</OnboardingSlideTransition>
|
||||
) : page === "owned" ? (
|
||||
) : page === "existing" ? (
|
||||
<OnboardingSlideTransition
|
||||
className="flex w-full flex-col items-center text-center"
|
||||
className="flex h-full min-h-0 w-full flex-col items-center text-center"
|
||||
containerClassName="h-full min-h-0 [&>.buzz-onboarding-transition-line]:h-full"
|
||||
direction={transitionDirection}
|
||||
transitionKey={`owned-${transitionDirection}`}
|
||||
>
|
||||
<HostedCommunityOnboarding onBack={() => showPage("welcome")} />
|
||||
</OnboardingSlideTransition>
|
||||
) : page === "join" ? (
|
||||
<OnboardingSlideTransition
|
||||
className="flex min-h-[calc(100dvh-15.625rem)] w-full flex-col items-center text-center"
|
||||
direction={transitionDirection}
|
||||
transitionKey={`join-${transitionDirection}`}
|
||||
transitionKey={`existing-${transitionDirection}`}
|
||||
>
|
||||
<div className="w-full max-w-[760px]">
|
||||
<h1
|
||||
aria-label="Request access to community"
|
||||
className="text-title font-normal"
|
||||
>
|
||||
Request access to a community
|
||||
<h1 className="text-title font-normal">
|
||||
Reconnect to your community
|
||||
</h1>
|
||||
<p className="mt-3 text-sm leading-6 text-foreground/80">
|
||||
Tell us your role so we can find the fastest way back in.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex w-full flex-1 items-center justify-center">
|
||||
<div className="w-full max-w-[920px] space-y-16">
|
||||
<section
|
||||
aria-labelledby="welcome-join-key-step"
|
||||
className="translate-y-12"
|
||||
<div className="flex w-full flex-1 translate-y-16 flex-col items-center justify-center gap-20 py-8">
|
||||
<Card
|
||||
asChild
|
||||
className={COMMUNITY_OPTION_CARD_CLASS}
|
||||
variant="textured"
|
||||
>
|
||||
<button
|
||||
data-testid="existing-choice-owner"
|
||||
onClick={() => setIsHostedSignInOpen(true)}
|
||||
type="button"
|
||||
>
|
||||
<h2
|
||||
className="relative z-10 mb-8 text-sm font-normal"
|
||||
id="welcome-join-key-step"
|
||||
>
|
||||
Step 1: Ask your community host to send you an invite link
|
||||
or add you using your public key
|
||||
</h2>
|
||||
<Card
|
||||
className={WIDE_TEXTURE_CARD_CLASS}
|
||||
data-testid="welcome-join-npub-frame"
|
||||
variant="textured"
|
||||
>
|
||||
<div className={WIDE_TEXTURE_CONTENT_CLASS}>
|
||||
<div className={ONBOARDING_KEY_ROW_CLASS}>
|
||||
<div className="min-w-0 flex-1">
|
||||
<code
|
||||
className={`${ONBOARDING_KEY_TEXT_CLASS} block`}
|
||||
data-testid="welcome-join-npub"
|
||||
>
|
||||
{npub || "Loading…"}
|
||||
</code>
|
||||
</div>
|
||||
<Button
|
||||
aria-label="Copy npub"
|
||||
className="h-10 w-10 shrink-0 text-[var(--buzz-onboarding-backup-ink)] hover:bg-transparent hover:text-foreground"
|
||||
disabled={!npub}
|
||||
onClick={() => {
|
||||
void writeTextToClipboard(npub).then(() => {
|
||||
setCopied(true);
|
||||
window.setTimeout(() => setCopied(false), 1500);
|
||||
});
|
||||
}}
|
||||
size="icon"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
{copied ? (
|
||||
<Check
|
||||
className="h-6 w-6 text-primary"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<Copy className="h-6 w-6" aria-hidden="true" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
{identityError ? (
|
||||
<p className="mt-4 text-sm text-destructive">
|
||||
{identityError}
|
||||
</p>
|
||||
) : null}
|
||||
</section>
|
||||
<form
|
||||
aria-labelledby="welcome-join-url-step"
|
||||
className="mx-auto w-full"
|
||||
id="welcome-join-form"
|
||||
onSubmit={handleJoin}
|
||||
I own the community
|
||||
</button>
|
||||
</Card>
|
||||
<Card
|
||||
asChild
|
||||
className={COMMUNITY_OPTION_CARD_CLASS}
|
||||
variant="textured"
|
||||
>
|
||||
<button
|
||||
data-testid="existing-choice-member"
|
||||
onClick={() => showPage("member")}
|
||||
type="button"
|
||||
>
|
||||
<h2
|
||||
className="relative z-10 mb-8 text-sm font-normal"
|
||||
id="welcome-join-url-step"
|
||||
>
|
||||
Step 2: Paste in your community URL
|
||||
</h2>
|
||||
<Card
|
||||
className={`${WIDE_TEXTURE_CARD_CLASS} [--buzz-card-textured-min-height:128px]`}
|
||||
variant="textured"
|
||||
>
|
||||
<div
|
||||
className={WIDE_TEXTURE_CONTENT_CLASS}
|
||||
style={HORIZONTAL_INPUT_OVERFLOW_FADE}
|
||||
>
|
||||
<Input
|
||||
aria-label="Community URL"
|
||||
autoCapitalize="none"
|
||||
autoCorrect="off"
|
||||
className="h-auto rounded-none border-0 bg-transparent p-0 text-center font-mono !text-4xl text-[color:var(--buzz-onboarding-backup-ink)] shadow-none placeholder:text-[color:var(--buzz-onboarding-backup-ink)] placeholder:opacity-10 focus-visible:ring-0"
|
||||
data-testid="welcome-join-community-url"
|
||||
id="welcome-join-community-url"
|
||||
onChange={(event) => {
|
||||
setRelayUrl(event.target.value);
|
||||
setRelayUrlError(null);
|
||||
}}
|
||||
placeholder="Enter community URL"
|
||||
spellCheck={false}
|
||||
type="url"
|
||||
value={relayUrl}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
{relayUrlError ? (
|
||||
<p className="mt-3 text-sm text-destructive">
|
||||
{relayUrlError}
|
||||
</p>
|
||||
) : null}
|
||||
</form>
|
||||
</div>
|
||||
I’m a member or admin
|
||||
</button>
|
||||
</Card>
|
||||
</div>
|
||||
<OnboardingFooter>
|
||||
<Button
|
||||
className={ONBOARDING_PRIMARY_CTA_CLASS}
|
||||
disabled={!relayUrl.trim()}
|
||||
form="welcome-join-form"
|
||||
type="submit"
|
||||
>
|
||||
<span aria-hidden>Next</span>
|
||||
<span className="sr-only">Join community</span>
|
||||
</Button>
|
||||
<Button
|
||||
className="h-9 rounded-full bg-foreground/10 px-6 hover:bg-foreground/15"
|
||||
data-testid="existing-back"
|
||||
onClick={() => showPage("welcome")}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
@@ -340,32 +217,57 @@ export function WelcomeSetup({
|
||||
</Button>
|
||||
</OnboardingFooter>
|
||||
</OnboardingSlideTransition>
|
||||
) : page === "owned" ? (
|
||||
<OnboardingSlideTransition
|
||||
className="flex w-full flex-col items-center text-center"
|
||||
direction={transitionDirection}
|
||||
transitionKey={`owned-${transitionDirection}`}
|
||||
>
|
||||
<HostedCommunityOnboarding onBack={() => showPage("welcome")} />
|
||||
</OnboardingSlideTransition>
|
||||
) : (
|
||||
<OnboardingSlideTransition
|
||||
className="flex min-h-[calc(100dvh-15.625rem)] w-full flex-col items-center text-center"
|
||||
direction={transitionDirection}
|
||||
transitionKey={`invite-${transitionDirection}`}
|
||||
transitionKey={`${page}-${transitionDirection}`}
|
||||
>
|
||||
<div className="w-full max-w-[500px]">
|
||||
<div className="w-full max-w-[620px]">
|
||||
<h1 className="text-title font-normal">
|
||||
Enter your invite link
|
||||
{page === "member"
|
||||
? "Reconnect to your community"
|
||||
: "Join a community"}
|
||||
</h1>
|
||||
<p className="mt-3 text-sm leading-6 text-foreground/80">
|
||||
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."}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex w-full flex-1 items-center justify-center">
|
||||
<InviteRedeemForm
|
||||
error={null}
|
||||
isRedeeming={false}
|
||||
onCancel={() => showPage("welcome")}
|
||||
onRedeem={handleInviteRedeem}
|
||||
onCancel={() =>
|
||||
showPage(page === "member" ? "existing" : "welcome")
|
||||
}
|
||||
onConnect={startConnection}
|
||||
onRedeem={redeemInvite}
|
||||
placeholder="Invite link or community URL"
|
||||
variant="onboarding-spotlight"
|
||||
/>
|
||||
</div>
|
||||
</OnboardingSlideTransition>
|
||||
)}
|
||||
{isHostedSignInOpen && page !== "owned" ? (
|
||||
<HostedCommunityOnboarding
|
||||
onBack={() => setIsHostedSignInOpen(false)}
|
||||
onReady={() => {
|
||||
setIsHostedSignInOpen(false);
|
||||
showPage("owned");
|
||||
}}
|
||||
stageHidden
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</OnboardingFooterProvider>
|
||||
</div>
|
||||
|
||||
@@ -26,7 +26,7 @@ export type CommunityOnboardingStage =
|
||||
*/
|
||||
| "entering";
|
||||
|
||||
export type FirstCommunityPage = "join" | "owned";
|
||||
export type FirstCommunityPage = "join" | "member" | "owned";
|
||||
|
||||
export type CommunityOnboardingTransaction = {
|
||||
id: string;
|
||||
|
||||
@@ -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
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -10491,6 +10491,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.
|
||||
|
||||
@@ -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("heading", { name: "Reconnect to your community" }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("button", {
|
||||
name: "Create or connect to my own 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),
|
||||
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",
|
||||
);
|
||||
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 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(() =>
|
||||
|
||||
Reference in New Issue
Block a user