fix(desktop): complete request access flow (#2073)

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:
Wes
2026-07-18 10:19:45 -07:00
committed by GitHub
co-authored by Pinky
parent 0dda0ad2f6
commit 426497a18e
3 changed files with 171 additions and 51 deletions
+49 -3
View File
@@ -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.
@@ -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<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);
const communityOnboarding = useCommunityOnboarding();
const systemColorScheme = useSystemColorScheme();
@@ -79,6 +83,24 @@ export function WelcomeSetup({
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",
relayUrl: normalizedRelayUrl,
});
},
[communityOnboarding, relayUrl],
);
const handleInviteRedeem = React.useCallback(
(relayWsUrl: string, code: string, policyReceipt?: string) => {
communityOnboarding.start({
@@ -172,58 +194,106 @@ export function WelcomeSetup({
</p>
</div>
<div className="flex w-full flex-1 items-center justify-center pb-4 pt-12">
<div className="w-full max-w-4xl">
<div
className={ONBOARDING_KEY_FRAME_CLASS}
data-testid="welcome-join-npub-frame"
>
<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"
<div className="w-full max-w-4xl space-y-7">
<div>
<div
className={ONBOARDING_KEY_FRAME_CLASS}
data-testid="welcome-join-npub-frame"
>
<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-muted-foreground hover:text-foreground"
disabled={!npub}
onClick={() => {
void writeTextToClipboard(npub).then(() => {
setCopied(true);
window.setTimeout(() => setCopied(false), 1500);
});
}}
size="icon"
type="button"
variant="ghost"
>
{npub || "Loading…"}
</code>
{copied ? (
<Check
className="h-6 w-6 text-primary"
aria-hidden="true"
/>
) : (
<Copy className="h-6 w-6" aria-hidden="true" />
)}
</Button>
</div>
</div>
{identityError ? (
<p className="mt-4 text-sm text-destructive">
{identityError}
</p>
) : (
<p className="mx-auto mt-4 flex max-w-[440px] items-start justify-center gap-1.5 text-center text-xs leading-5 text-[var(--buzz-onboarding-backup-ink)]">
<Info className="mt-0.5 h-3.5 w-3.5 shrink-0" />
<span>
This is safe to share. It does not reveal your private
key.
</span>
</p>
)}
</div>
<form
className="mx-auto w-full max-w-[680px] text-left"
onSubmit={handleJoin}
>
<label
className="text-sm font-medium"
htmlFor="welcome-join-community-url"
>
Community URL
</label>
<div className="mt-2 flex items-start gap-3">
<div className="min-w-0 flex-1">
<Input
autoCapitalize="none"
autoCorrect="off"
data-testid="welcome-join-community-url"
id="welcome-join-community-url"
onChange={(event) => {
setRelayUrl(event.target.value);
setRelayUrlError(null);
}}
placeholder="https://community.example.com"
spellCheck={false}
type="url"
value={relayUrl}
/>
{relayUrlError ? (
<p className="mt-2 text-sm text-destructive">
{relayUrlError}
</p>
) : (
<p className="mt-2 text-xs leading-5 text-foreground/70">
Once the host adds your public key, enter the URL to
join. You can retry if access is not ready yet.
</p>
)}
</div>
<Button
aria-label="Copy npub"
className="h-10 w-10 shrink-0 text-muted-foreground hover:text-foreground"
disabled={!npub}
onClick={() => {
void writeTextToClipboard(npub).then(() => {
setCopied(true);
window.setTimeout(() => setCopied(false), 1500);
});
}}
size="icon"
type="button"
variant="ghost"
className="h-10 shrink-0 rounded-full px-6"
disabled={!relayUrl.trim()}
type="submit"
>
{copied ? (
<Check
className="h-6 w-6 text-primary"
aria-hidden="true"
/>
) : (
<Copy className="h-6 w-6" aria-hidden="true" />
)}
Join community
</Button>
</div>
</div>
{identityError ? (
<p className="mt-4 text-sm text-destructive">
{identityError}
</p>
) : (
<p className="mx-auto mt-6 flex max-w-[440px] items-start justify-center gap-1.5 text-center text-xs leading-5 text-[var(--buzz-onboarding-backup-ink)]">
<Info className="mt-0.5 h-3.5 w-3.5 shrink-0" />
<span>
This is safe to share. It does not reveal your private
key.
</span>
</p>
)}
</form>
</div>
</div>
<OnboardingFooter>
@@ -190,7 +190,11 @@ export function CommunityOnboardingFlow({
};
}, [clear, isEnteringStage]);
const retryClaim = () => update({ stage: "claiming", error: undefined });
const retry = () =>
update({
stage: transaction?.inviteCode ? "claiming" : "connecting",
error: undefined,
});
const relayUrl = transaction?.relayUrl;
const finish = React.useCallback(async () => {
if (!relayUrl) return;
@@ -362,7 +366,7 @@ export function CommunityOnboardingFlow({
</p>
<div className="mt-6 flex justify-center gap-3">
{transaction.error ? (
<Button className="rounded-full px-6" onClick={retryClaim}>
<Button className="rounded-full px-6" onClick={retry}>
Retry
</Button>
) : null}