From a4446c44f7f6809f245c2f582ef160e69d814b35 Mon Sep 17 00:00:00 2001 From: npub1rf6fvdj6ut0c4kcmjv4p5mmgh89nj58n69uu3fz3cvk3jn500hqs7emz79 <1a7496365ae2df8adb1b932a1a6f68b9cb3950f3d179c8a451c32d194e8f7dc1@sprout-oss.stage.blox.sqprod.co> Date: Sun, 19 Jul 2026 19:36:46 -0700 Subject: [PATCH] fix(desktop): reuse profiles when joining communities Co-authored-by: npub1rf6fvdj6ut0c4kcmjv4p5mmgh89nj58n69uu3fz3cvk3jn500hqs7emz79 <1a7496365ae2df8adb1b932a1a6f68b9cb3950f3d179c8a451c32d194e8f7dc1@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1rf6fvdj6ut0c4kcmjv4p5mmgh89nj58n69uu3fz3cvk3jn500hqs7emz79 <1a7496365ae2df8adb1b932a1a6f68b9cb3950f3d179c8a451c32d194e8f7dc1@sprout-oss.stage.blox.sqprod.co> --- .../onboarding/ui/CommunityOnboardingFlow.tsx | 19 +++++- desktop/src/testing/e2eBridge.ts | 9 +++ desktop/tests/e2e/onboarding.spec.ts | 60 +++++++++++++++++++ desktop/tests/helpers/bridge.ts | 2 + 4 files changed, 89 insertions(+), 1 deletion(-) diff --git a/desktop/src/features/onboarding/ui/CommunityOnboardingFlow.tsx b/desktop/src/features/onboarding/ui/CommunityOnboardingFlow.tsx index fabd7eef83..4aa1b2adde 100644 --- a/desktop/src/features/onboarding/ui/CommunityOnboardingFlow.tsx +++ b/desktop/src/features/onboarding/ui/CommunityOnboardingFlow.tsx @@ -19,7 +19,7 @@ import { parseEmojiAvatarDataUrl, ProfileAvatarEditor, } from "@/features/profile/ui/ProfileAvatarEditor"; -import { updateProfile } from "@/shared/api/tauriProfiles"; +import { getProfile, updateProfile } from "@/shared/api/tauriProfiles"; import { getIdentity, importIdentity } from "@/shared/api/tauriIdentity"; import { listPersonas } from "@/shared/api/tauriPersonas"; import { relayClient } from "@/shared/api/relayClient"; @@ -151,6 +151,7 @@ export function CommunityOnboardingFlow({ [], ); const [isPending, setIsPending] = React.useState(false); + const checkedProfileTransactionRef = React.useRef(null); const [starterChannelFailureCount, setStarterChannelFailureCount] = React.useState(0); const [deniedPubkey, setDeniedPubkey] = React.useState(""); @@ -274,6 +275,22 @@ export function CommunityOnboardingFlow({ }, [isPending, update]); const isProfileStage = transaction?.stage === "profile"; + React.useEffect(() => { + if (!isProfileStage || !transaction) return; + if (checkedProfileTransactionRef.current === transaction.id) return; + + checkedProfileTransactionRef.current = transaction.id; + void getProfile() + .then((profile) => { + if (profile.hasProfileEvent) { + update({ stage: "team-intro", error: undefined }, transaction.id); + } + }) + .catch(() => { + // Discovery is best-effort. Staying on the profile step preserves the + // existing path when the relay cannot answer the lookup. + }); + }, [isProfileStage, transaction, update]); const isTeamStage = transaction?.stage === "team-intro" || transaction?.stage === "finalizing" || diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts index c3dcf4db12..a9af685d73 100644 --- a/desktop/src/testing/e2eBridge.ts +++ b/desktop/src/testing/e2eBridge.ts @@ -216,6 +216,8 @@ type E2eConfig = { channelWindowDelayMs?: number; profileReadDelayMs?: number; profileReadError?: string; + /** Override whether get_profile reports a real kind:0 event. */ + profileHasEvent?: boolean; profileUpdateError?: string; profileUpdateErrors?: string[]; searchProfiles?: MockSearchProfileSeed[]; @@ -5088,6 +5090,13 @@ async function handleGetChannels(config: E2eConfig | undefined) { async function handleGetProfile(config: E2eConfig | undefined) { const identity = getIdentity(config); + const forcedHasProfileEvent = config?.mock?.profileHasEvent; + if (forcedHasProfileEvent !== undefined) { + return { + ...cloneProfile(ensureMockProfile(config)), + has_profile_event: forcedHasProfileEvent, + }; + } if (!identity) { const profileReadDelayMs = config?.mock?.profileReadDelayMs ?? 0; if (profileReadDelayMs > 0) { diff --git a/desktop/tests/e2e/onboarding.spec.ts b/desktop/tests/e2e/onboarding.spec.ts index 6a057c8ecc..4a212fdb8c 100644 --- a/desktop/tests/e2e/onboarding.spec.ts +++ b/desktop/tests/e2e/onboarding.spec.ts @@ -1170,6 +1170,66 @@ test("first-community direct join reaches profile", async ({ page }) => { .toEqual({ communityCount: 1, transactionMatchesOnlyCommunity: true }); }); +test("community onboarding reuses an existing relay profile", async ({ + page, +}) => { + await seedActiveIdentity(page, BLANK_TYLER_IDENTITY); + await page.addInitScript( + ({ pubkey, transactionStorageKey }) => { + window.localStorage.setItem( + `buzz-machine-onboarding-complete.v2:${pubkey}`, + "true", + ); + const timestamp = new Date().toISOString(); + window.localStorage.setItem( + transactionStorageKey, + JSON.stringify({ + id: "txn-existing-profile", + source: "add-community", + stage: "profile", + relayUrl: "wss://onboarding.communities.buzz.xyz", + communityName: "Onboarding", + communityId: "e2e-default-community", + createdAt: timestamp, + updatedAt: timestamp, + }), + ); + }, + { + pubkey: BLANK_TYLER_IDENTITY.pubkey, + transactionStorageKey: COMMUNITY_ONBOARDING_TRANSACTION_STORAGE_KEY, + }, + ); + await installMockBridge( + page, + { profileHasEvent: true }, + { + relayWsUrl: "wss://onboarding.communities.buzz.xyz", + skipOnboardingSeed: true, + }, + ); + await page.goto("/"); + + await expect + .poll(() => + page.evaluate( + () => + ( + window as Window & { __BUZZ_E2E_COMMANDS__?: string[] } + ).__BUZZ_E2E_COMMANDS__?.filter( + (command) => command === "get_profile", + ).length ?? 0, + ), + ) + .toBeGreaterThan(0); + await expect( + page.getByRole("heading", { name: "Meet your starter team" }), + ).toBeVisible(); + await expect( + page.getByRole("heading", { name: "Build your profile" }), + ).toHaveCount(0); +}); + test("first-community direct join cancel returns to request access", async ({ page, }) => { diff --git a/desktop/tests/helpers/bridge.ts b/desktop/tests/helpers/bridge.ts index 75a0aca901..b80cf47dc7 100644 --- a/desktop/tests/helpers/bridge.ts +++ b/desktop/tests/helpers/bridge.ts @@ -225,6 +225,8 @@ type MockBridgeOptions = { channelWindowDelayMs?: number; profileReadDelayMs?: number; profileReadError?: string; + /** Override whether get_profile reports a real kind:0 event. */ + profileHasEvent?: boolean; profileUpdateError?: string; profileUpdateErrors?: string[]; searchProfiles?: MockSearchProfileSeed[];