mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(desktop): recover first community joins (#2087)
Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
+66
-51
@@ -39,7 +39,6 @@ import { WelcomeSetup } from "@/features/communities/ui/WelcomeSetup";
|
||||
import { CommunityApplyErrorScreen } from "@/features/communities/ui/CommunityApplyErrorScreen";
|
||||
import { CommunityChangeOverlay } from "@/features/communities/ui/CommunityChangeOverlay";
|
||||
import { createBuzzQueryClient } from "@/shared/api/queryClient";
|
||||
import { getMyRelayMembershipLookup } from "@/shared/api/relayMembers";
|
||||
import { isSharedIdentity as isSharedIdentityCmd } from "@/shared/api/tauri";
|
||||
import {
|
||||
type AddCommunityDeepLinkPayload,
|
||||
@@ -62,16 +61,6 @@ const BOOT_SPLASH_MIN_VISIBLE_MS = 1_200;
|
||||
const BOOT_SPLASH_FADE_MS = 200;
|
||||
const INITIAL_RENDER_READY_EVENT = "initial-render-ready";
|
||||
|
||||
function isRelayMembershipDeniedError(error: unknown): boolean {
|
||||
if (!(error instanceof Error)) return false;
|
||||
return [
|
||||
"You must be a relay member",
|
||||
"relay_membership_required",
|
||||
"restricted: not a relay member",
|
||||
"invalid: you are not a relay member",
|
||||
].some((message) => error.message.includes(message));
|
||||
}
|
||||
|
||||
type BootSplashPhase = "holding" | "fading" | "done";
|
||||
|
||||
function useInitialRenderReady() {
|
||||
@@ -275,13 +264,19 @@ function CommunityApp({
|
||||
}) {
|
||||
const {
|
||||
activeCommunity,
|
||||
communities,
|
||||
reinitKey,
|
||||
addCommunity,
|
||||
clearCommunities,
|
||||
removeCommunity,
|
||||
switchCommunity,
|
||||
reconnectCommunity,
|
||||
} = useCommunities();
|
||||
const communityOnboarding = useCommunityOnboarding();
|
||||
const connectingTransactionRef = useRef<string | null>(null);
|
||||
const [isCommunityChangeOpen, setIsCommunityChangeOpen] = useState(false);
|
||||
const [resumeFirstCommunityJoin, setResumeFirstCommunityJoin] =
|
||||
useState(false);
|
||||
|
||||
// Surface nest-related backend events (repos-dir errors, legacy migration)
|
||||
// as toasts. Mounted before useCommunityInit so the listeners are registered
|
||||
@@ -311,10 +306,16 @@ function CommunityApp({
|
||||
const handleCommunityOnboardingConnect = useCallback(() => {
|
||||
const transaction = communityOnboarding.transaction;
|
||||
if (transaction?.stage !== "connecting") return;
|
||||
if (connectingTransactionRef.current === transaction.id) return;
|
||||
connectingTransactionRef.current = transaction.id;
|
||||
if (transaction.communityId) {
|
||||
switchCommunity(transaction.communityId);
|
||||
return;
|
||||
}
|
||||
const previousCommunityId = activeCommunity?.id;
|
||||
const relayAlreadyExists = communities.some(
|
||||
(community) => community.relayUrl === transaction.relayUrl,
|
||||
);
|
||||
const id = addCommunity({
|
||||
id: crypto.randomUUID(),
|
||||
name: transaction.communityName,
|
||||
@@ -323,58 +324,70 @@ function CommunityApp({
|
||||
reposDir: transaction.reposDir,
|
||||
addedAt: new Date().toISOString(),
|
||||
});
|
||||
communityOnboarding.update({ communityId: id, error: undefined });
|
||||
communityOnboarding.update({
|
||||
communityId: id,
|
||||
previousCommunityId,
|
||||
addedCommunity: !relayAlreadyExists,
|
||||
error: undefined,
|
||||
});
|
||||
switchCommunity(id);
|
||||
reconnectCommunity();
|
||||
}, [addCommunity, communityOnboarding, reconnectCommunity, switchCommunity]);
|
||||
}, [
|
||||
activeCommunity?.id,
|
||||
addCommunity,
|
||||
communities,
|
||||
communityOnboarding,
|
||||
reconnectCommunity,
|
||||
switchCommunity,
|
||||
]);
|
||||
|
||||
const handleCommunityOnboardingCancel = useCallback(() => {
|
||||
const transaction = communityOnboarding.transaction;
|
||||
communityOnboarding.clear();
|
||||
|
||||
if (!transaction?.communityId) return;
|
||||
if (!transaction.addedCommunity) {
|
||||
if (transaction.previousCommunityId) {
|
||||
switchCommunity(transaction.previousCommunityId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (communities.length === 1) {
|
||||
if (transaction.source === "first-community") {
|
||||
setResumeFirstCommunityJoin(true);
|
||||
}
|
||||
clearCommunities();
|
||||
return;
|
||||
}
|
||||
removeCommunity(transaction.communityId);
|
||||
if (transaction.previousCommunityId) {
|
||||
switchCommunity(transaction.previousCommunityId);
|
||||
}
|
||||
}, [
|
||||
clearCommunities,
|
||||
communities.length,
|
||||
communityOnboarding,
|
||||
removeCommunity,
|
||||
switchCommunity,
|
||||
]);
|
||||
|
||||
const bootSplashPhase = useBootSplashHold();
|
||||
|
||||
const transaction = communityOnboarding.transaction;
|
||||
useEffect(() => {
|
||||
if (transaction?.stage !== "connecting") {
|
||||
connectingTransactionRef.current = null;
|
||||
}
|
||||
}, [transaction?.stage]);
|
||||
const targetIsReady =
|
||||
transaction?.communityId === activeCommunity?.id &&
|
||||
community.isReady &&
|
||||
community.appliedKey === communityKey;
|
||||
useEffect(() => {
|
||||
if (
|
||||
transaction?.stage !== "connecting" ||
|
||||
transaction.error ||
|
||||
!targetIsReady
|
||||
) {
|
||||
return;
|
||||
if (transaction?.stage === "connecting" && targetIsReady) {
|
||||
communityOnboarding.update({ stage: "profile", error: undefined });
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
void getMyRelayMembershipLookup()
|
||||
.then(({ membership, snapshotFound }) => {
|
||||
if (cancelled) return;
|
||||
if (snapshotFound && membership === null) {
|
||||
communityOnboarding.update({
|
||||
error:
|
||||
"You have not been added to this community yet. Ask the host to add your public key, then try again.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
communityOnboarding.update({ stage: "profile", error: undefined });
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
if (cancelled) return;
|
||||
communityOnboarding.update({
|
||||
error: isRelayMembershipDeniedError(error)
|
||||
? "You have not been added to this community yet. Ask the host to add your public key, then try again."
|
||||
: "Could not check community access. Check the URL and try again.",
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [
|
||||
communityOnboarding.update,
|
||||
targetIsReady,
|
||||
transaction?.error,
|
||||
transaction?.stage,
|
||||
]);
|
||||
}, [communityOnboarding.update, targetIsReady, transaction?.stage]);
|
||||
// During "entering" the transaction stays alive as a curtain: the app mounts
|
||||
// underneath (already pointed at the Welcome channel route) while the
|
||||
// onboarding screen covers it, then fades once Welcome reports ready.
|
||||
@@ -398,6 +411,7 @@ function CommunityApp({
|
||||
appContent = (
|
||||
<WelcomeSetup
|
||||
defaultRelayUrl={community.defaultRelayUrl}
|
||||
initialPage={resumeFirstCommunityJoin ? "join" : undefined}
|
||||
onBack={onBackToMachineConfig}
|
||||
/>
|
||||
);
|
||||
@@ -465,6 +479,7 @@ function CommunityApp({
|
||||
}
|
||||
>
|
||||
<CommunityOnboardingFlow
|
||||
onCancel={handleCommunityOnboardingCancel}
|
||||
onConnect={handleCommunityOnboardingConnect}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -32,6 +32,7 @@ type WelcomeTransitionMode = "initial" | OnboardingTransitionDirection;
|
||||
|
||||
type WelcomeSetupProps = {
|
||||
defaultRelayUrl: string;
|
||||
initialPage?: WelcomeSetupPage;
|
||||
initialTransitionMode?: WelcomeTransitionMode;
|
||||
onBack: () => void;
|
||||
};
|
||||
@@ -50,10 +51,11 @@ function isLocalDevRelayUrl(relayUrl: string) {
|
||||
|
||||
export function WelcomeSetup({
|
||||
defaultRelayUrl,
|
||||
initialPage = "welcome",
|
||||
initialTransitionMode = "initial",
|
||||
onBack,
|
||||
}: WelcomeSetupProps) {
|
||||
const [page, setPage] = React.useState<WelcomeSetupPage>("welcome");
|
||||
const [page, setPage] = React.useState<WelcomeSetupPage>(initialPage);
|
||||
const [transitionMode, setTransitionMode] =
|
||||
React.useState<WelcomeTransitionMode>(initialTransitionMode);
|
||||
const [npub, setNpub] = React.useState("");
|
||||
|
||||
@@ -42,6 +42,8 @@ export type CommunityOnboardingTransaction = {
|
||||
*/
|
||||
policyReceipt?: string;
|
||||
communityId?: string;
|
||||
previousCommunityId?: string;
|
||||
addedCommunity?: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
error?: string;
|
||||
@@ -56,6 +58,8 @@ export type CommunityOnboardingTransactionPatch = Partial<
|
||||
| "stage"
|
||||
| "relayUrl"
|
||||
| "communityId"
|
||||
| "previousCommunityId"
|
||||
| "addedCommunity"
|
||||
| "communityName"
|
||||
| "error"
|
||||
| "acknowledged"
|
||||
|
||||
@@ -113,8 +113,10 @@ function AvatarCircle({
|
||||
}
|
||||
|
||||
export function CommunityOnboardingFlow({
|
||||
onCancel,
|
||||
onConnect,
|
||||
}: {
|
||||
onCancel: () => void;
|
||||
onConnect: () => void;
|
||||
}) {
|
||||
const { transaction, update, clear } = useCommunityOnboarding();
|
||||
@@ -372,7 +374,7 @@ export function CommunityOnboardingFlow({
|
||||
) : null}
|
||||
<Button
|
||||
className="rounded-full bg-foreground/10 px-5 hover:bg-foreground/15"
|
||||
onClick={clear}
|
||||
onClick={onCancel}
|
||||
variant="ghost"
|
||||
>
|
||||
Cancel
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
persistCurrentIdentity,
|
||||
} from "@/shared/api/tauriIdentity";
|
||||
import { useSystemColorScheme } from "@/shared/theme/useSystemColorScheme";
|
||||
import { forceFreshOnboarding } from "@/features/onboarding/devFreshOnboarding";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { StartupWindowDragRegion } from "@/shared/ui/StartupWindowDragRegion";
|
||||
import { AvatarStep } from "./AvatarStep";
|
||||
@@ -513,7 +512,7 @@ export function OnboardingFlow({
|
||||
}}
|
||||
direction={transitionDirection}
|
||||
state={profileStepState}
|
||||
usesExistingIdentity={forceFreshOnboarding}
|
||||
usesExistingIdentity
|
||||
/>
|
||||
) : currentPage === "key-import" ? (
|
||||
<OnboardingSlideTransition
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { hexToBytes } from "@noble/hashes/utils.js";
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { nsecEncode } from "nostr-tools/nip19";
|
||||
|
||||
import { installMockBridge, TEST_IDENTITIES } from "../helpers/bridge";
|
||||
import { waitForAnimations } from "../helpers/animations";
|
||||
@@ -82,36 +80,19 @@ test("avatar Next button still requires an avatar to be chosen", async ({
|
||||
// B4: Routing tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("import-key path skips backup and goes directly to avatar", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Import tyler's OWN key (same pubkey = no component remount) so the
|
||||
// identityWasImported flag persists in the same component instance.
|
||||
test("normal profile setup keeps the existing identity", async ({ page }) => {
|
||||
await seedActiveIdentity(page, BLANK_TYLER_IDENTITY);
|
||||
await installMockBridge(page, undefined, { skipOnboardingSeed: true });
|
||||
await page.goto("/");
|
||||
|
||||
// Profile page — click "Use existing key" to open the key import form.
|
||||
await expect(page.getByTestId("onboarding-page-1")).toBeVisible();
|
||||
await page.getByTestId("onboarding-import-key").click();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Use your existing key" }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByTestId("onboarding-import-key")).toHaveCount(0);
|
||||
await expect(page.getByText("Create an identity key")).toHaveCount(0);
|
||||
|
||||
// Enter tyler's own nsec (same pubkey → no remount, identityWasImported stays true).
|
||||
const tylerNsec = nsecEncode(hexToBytes(TEST_IDENTITIES.tyler.privateKey));
|
||||
await page.getByTestId("nostr-import-nsec-input").fill(tylerNsec);
|
||||
await expect(page.getByTestId("nostr-import-npub-preview")).toBeVisible();
|
||||
await page.getByTestId("nostr-import-submit").click();
|
||||
|
||||
// After import, the flow returns to profile with identityWasImported=true.
|
||||
await expect(page.getByTestId("onboarding-page-1")).toBeVisible();
|
||||
await page.getByTestId("onboarding-display-name").fill("Morty QA");
|
||||
await page.getByTestId("onboarding-next").click();
|
||||
|
||||
// Backup page must NOT appear — avatar comes next on the import path.
|
||||
await expect(page.getByTestId("onboarding-page-avatar")).toBeVisible();
|
||||
await expect(page.getByTestId("onboarding-page-backup")).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("Back from the community avatar step returns to profile", async ({
|
||||
|
||||
@@ -767,6 +767,184 @@ test("first-community shows the scenario cards for localhost", async ({
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("first-community direct join reaches profile", 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, undefined, {
|
||||
relayWsUrl: "wss://onboarding.communities.buzz.xyz",
|
||||
skipOnboardingSeed: true,
|
||||
skipCommunitySeed: true,
|
||||
});
|
||||
await page.goto("/");
|
||||
|
||||
await page.getByRole("button", { name: "Add me to a community" }).click();
|
||||
await page
|
||||
.getByTestId("welcome-join-community-url")
|
||||
.fill("wss://onboarding.communities.buzz.xyz");
|
||||
await page.getByRole("button", { name: "Join community" }).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Build your profile" }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText("Connecting securely…")).toHaveCount(0);
|
||||
await expect(page.getByText("Create an identity key")).toHaveCount(0);
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate((transactionStorageKey) => {
|
||||
const communitiesRaw = window.localStorage.getItem("buzz-communities");
|
||||
const transactionRaw = window.localStorage.getItem(
|
||||
transactionStorageKey,
|
||||
);
|
||||
const communities = communitiesRaw
|
||||
? (JSON.parse(communitiesRaw) as Array<{ id: string }>)
|
||||
: [];
|
||||
const transaction = transactionRaw
|
||||
? (JSON.parse(transactionRaw) as { communityId?: string })
|
||||
: null;
|
||||
return {
|
||||
communityCount: communities.length,
|
||||
transactionMatchesOnlyCommunity:
|
||||
communities.length === 1 &&
|
||||
transaction?.communityId === communities[0]?.id,
|
||||
};
|
||||
}, COMMUNITY_ONBOARDING_TRANSACTION_STORAGE_KEY),
|
||||
)
|
||||
.toEqual({ communityCount: 1, transactionMatchesOnlyCommunity: true });
|
||||
});
|
||||
|
||||
test("first-community direct join cancel returns to request access", 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,
|
||||
{ applyCommunityDelayMs: 5_000 },
|
||||
{
|
||||
relayWsUrl: "wss://onboarding.communities.buzz.xyz",
|
||||
skipOnboardingSeed: true,
|
||||
skipCommunitySeed: true,
|
||||
},
|
||||
);
|
||||
await page.goto("/");
|
||||
|
||||
await page.getByRole("button", { name: "Add me to a community" }).click();
|
||||
await page
|
||||
.getByTestId("welcome-join-community-url")
|
||||
.fill("wss://onboarding.communities.buzz.xyz");
|
||||
await page.getByRole("button", { name: "Join community" }).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" }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByTestId("community-change-overlay")).toHaveCount(0);
|
||||
await expect(page.getByText("Create an identity key")).toHaveCount(0);
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(
|
||||
(storageKey) => ({
|
||||
communities: window.localStorage.getItem("buzz-communities"),
|
||||
transaction: window.localStorage.getItem(storageKey),
|
||||
}),
|
||||
COMMUNITY_ONBOARDING_TRANSACTION_STORAGE_KEY,
|
||||
),
|
||||
)
|
||||
.toEqual({ communities: null, transaction: null });
|
||||
});
|
||||
|
||||
test("canceling a join to an existing inactive community preserves it", async ({
|
||||
page,
|
||||
}) => {
|
||||
await seedActiveIdentity(page, BLANK_TYLER_IDENTITY);
|
||||
await page.addInitScript(
|
||||
({ pubkey, relayUrl }) => {
|
||||
window.localStorage.setItem(
|
||||
`buzz-machine-onboarding-complete.v2:${pubkey}`,
|
||||
"true",
|
||||
);
|
||||
const timestamp = new Date().toISOString();
|
||||
window.localStorage.setItem(
|
||||
"buzz-communities",
|
||||
JSON.stringify([
|
||||
{
|
||||
id: "active-community",
|
||||
name: "Active",
|
||||
relayUrl: "wss://active.example.com",
|
||||
addedAt: timestamp,
|
||||
},
|
||||
{
|
||||
id: "existing-community",
|
||||
name: "Existing",
|
||||
relayUrl,
|
||||
addedAt: timestamp,
|
||||
},
|
||||
]),
|
||||
);
|
||||
window.localStorage.setItem(
|
||||
"buzz-active-community-id",
|
||||
"active-community",
|
||||
);
|
||||
},
|
||||
{
|
||||
pubkey: BLANK_TYLER_IDENTITY.pubkey,
|
||||
relayUrl: "wss://onboarding.communities.buzz.xyz",
|
||||
},
|
||||
);
|
||||
await installMockBridge(
|
||||
page,
|
||||
{ applyCommunityDelayMs: 5_000 },
|
||||
{
|
||||
relayWsUrl: "wss://active.example.com",
|
||||
skipOnboardingSeed: true,
|
||||
skipCommunitySeed: true,
|
||||
},
|
||||
);
|
||||
await page.goto("/");
|
||||
|
||||
await page.evaluate((transactionStorageKey) => {
|
||||
const timestamp = new Date().toISOString();
|
||||
window.localStorage.setItem(
|
||||
transactionStorageKey,
|
||||
JSON.stringify({
|
||||
id: "existing-community-join",
|
||||
source: "add-community",
|
||||
stage: "connecting",
|
||||
relayUrl: "wss://onboarding.communities.buzz.xyz",
|
||||
communityName: "Existing",
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
}),
|
||||
);
|
||||
window.location.reload();
|
||||
}, COMMUNITY_ONBOARDING_TRANSACTION_STORAGE_KEY);
|
||||
|
||||
await expect(page.getByText("Connecting securely…")).toBeVisible();
|
||||
await page.getByRole("button", { name: "Cancel" }).click();
|
||||
await expect(page.getByText("Connecting securely…")).toHaveCount(0);
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(() => {
|
||||
const raw = window.localStorage.getItem("buzz-communities");
|
||||
return raw
|
||||
? (JSON.parse(raw) as Array<{ id: string }>).map(({ id }) => id)
|
||||
: [];
|
||||
}),
|
||||
)
|
||||
.toEqual(["active-community", "existing-community"]);
|
||||
});
|
||||
|
||||
test("connected first-community profile step cannot discard resumable onboarding", async ({
|
||||
page,
|
||||
}) => {
|
||||
@@ -1400,31 +1578,19 @@ test("existing relay profile with display name auto-skips onboarding without loc
|
||||
await expectHomeView(page);
|
||||
});
|
||||
|
||||
test("onboarding can import an existing key when the community is already set up", async ({
|
||||
test("onboarding uses the existing identity when the community is already set up", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Community exists (default seed), but this identity has no profile yet,
|
||||
// so the app lands on the onboarding name step — Tyler's moved-laptop /
|
||||
// fresh-dev-instance case.
|
||||
// Community exists (default seed), and machine onboarding has already created
|
||||
// this identity. Profile setup must not offer to create or replace it.
|
||||
await seedActiveIdentity(page, BLANK_TYLER_IDENTITY);
|
||||
await installMockBridge(page, undefined, { skipOnboardingSeed: true });
|
||||
await page.goto("/");
|
||||
|
||||
await expect(page.getByTestId("onboarding-display-name")).toHaveValue("");
|
||||
await page.getByTestId("onboarding-import-key").click();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Use your existing key" }),
|
||||
).toBeVisible();
|
||||
|
||||
const importedNsec = nsecEncode(hexToBytes(TEST_IDENTITIES.alice.privateKey));
|
||||
await page.getByTestId("nostr-import-nsec-input").fill(importedNsec);
|
||||
await expect(page.getByTestId("nostr-import-npub-preview")).toBeVisible();
|
||||
await page.getByTestId("nostr-import-submit").click();
|
||||
|
||||
// Identity swap remounts the flow; alice already has a relay profile with
|
||||
// a display name, so onboarding auto-completes and lands in the app.
|
||||
await expect(page.getByTestId("onboarding-gate")).toHaveCount(0);
|
||||
await expectHomeView(page);
|
||||
await expect(page.getByTestId("onboarding-next")).toHaveText("Continue");
|
||||
await expect(page.getByTestId("onboarding-import-key")).toHaveCount(0);
|
||||
await expect(page.getByText("Create an identity key")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("completed onboarding backfills missing starter channels", async ({
|
||||
|
||||
Reference in New Issue
Block a user