diff --git a/desktop/src/app/App.tsx b/desktop/src/app/App.tsx
index a4b44d2974..0fe740ebc3 100644
--- a/desktop/src/app/App.tsx
+++ b/desktop/src/app/App.tsx
@@ -39,6 +39,7 @@ 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,
@@ -61,6 +62,16 @@ 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() {
@@ -325,10 +336,45 @@ function CommunityApp({
community.isReady &&
community.appliedKey === communityKey;
useEffect(() => {
- if (transaction?.stage === "connecting" && targetIsReady) {
- communityOnboarding.update({ stage: "profile", error: undefined });
+ if (
+ transaction?.stage !== "connecting" ||
+ transaction.error ||
+ !targetIsReady
+ ) {
+ return;
}
- }, [communityOnboarding.update, targetIsReady, transaction?.stage]);
+
+ 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,
+ ]);
// 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.
diff --git a/desktop/src/features/communities/ui/WelcomeSetup.tsx b/desktop/src/features/communities/ui/WelcomeSetup.tsx
index c4412bd2d9..7c4dbdaaec 100644
--- a/desktop/src/features/communities/ui/WelcomeSetup.tsx
+++ b/desktop/src/features/communities/ui/WelcomeSetup.tsx
@@ -3,6 +3,7 @@ import { openUrl } from "@tauri-apps/plugin-opener";
import { Check, Copy, Info } from "lucide-react";
import { useCommunityOnboarding } from "@/features/onboarding/communityOnboarding";
+import { normalizeRelayUrl } from "@/features/communities/relayProbe";
import { InviteRedeemForm } from "@/features/onboarding/ui/InviteRedeemForm";
import {
ONBOARDING_KEY_FRAME_CLASS,
@@ -20,6 +21,7 @@ import {
import { getIdentity } from "@/shared/api/tauriIdentity";
import { pubkeyToNpub } from "@/shared/lib/nostrUtils";
import { Button } from "@/shared/ui/button";
+import { Input } from "@/shared/ui/input";
import { StartupWindowDragRegion } from "@/shared/ui/StartupWindowDragRegion";
import { useSystemColorScheme } from "@/shared/theme/useSystemColorScheme";
import { OnboardingChrome } from "@/features/onboarding/ui/OnboardingChrome";
@@ -56,6 +58,8 @@ export function WelcomeSetup({
React.useState
+ {npub || "Loading…"}
+
+ + {identityError} +
+ ) : ( +
+