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 { CommunityApplyErrorScreen } from "@/features/communities/ui/CommunityApplyErrorScreen";
import { CommunityChangeOverlay } from "@/features/communities/ui/CommunityChangeOverlay"; import { CommunityChangeOverlay } from "@/features/communities/ui/CommunityChangeOverlay";
import { createBuzzQueryClient } from "@/shared/api/queryClient"; import { createBuzzQueryClient } from "@/shared/api/queryClient";
import { getMyRelayMembershipLookup } from "@/shared/api/relayMembers";
import { isSharedIdentity as isSharedIdentityCmd } from "@/shared/api/tauri"; import { isSharedIdentity as isSharedIdentityCmd } from "@/shared/api/tauri";
import { import {
type AddCommunityDeepLinkPayload, type AddCommunityDeepLinkPayload,
@@ -61,6 +62,16 @@ const BOOT_SPLASH_MIN_VISIBLE_MS = 1_200;
const BOOT_SPLASH_FADE_MS = 200; const BOOT_SPLASH_FADE_MS = 200;
const INITIAL_RENDER_READY_EVENT = "initial-render-ready"; 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"; type BootSplashPhase = "holding" | "fading" | "done";
function useInitialRenderReady() { function useInitialRenderReady() {
@@ -325,10 +336,45 @@ function CommunityApp({
community.isReady && community.isReady &&
community.appliedKey === communityKey; community.appliedKey === communityKey;
useEffect(() => { useEffect(() => {
if (transaction?.stage === "connecting" && targetIsReady) { if (
communityOnboarding.update({ stage: "profile", error: undefined }); 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 // During "entering" the transaction stays alive as a curtain: the app mounts
// underneath (already pointed at the Welcome channel route) while the // underneath (already pointed at the Welcome channel route) while the
// onboarding screen covers it, then fades once Welcome reports ready. // 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 { Check, Copy, Info } from "lucide-react";
import { useCommunityOnboarding } from "@/features/onboarding/communityOnboarding"; import { useCommunityOnboarding } from "@/features/onboarding/communityOnboarding";
import { normalizeRelayUrl } from "@/features/communities/relayProbe";
import { InviteRedeemForm } from "@/features/onboarding/ui/InviteRedeemForm"; import { InviteRedeemForm } from "@/features/onboarding/ui/InviteRedeemForm";
import { import {
ONBOARDING_KEY_FRAME_CLASS, ONBOARDING_KEY_FRAME_CLASS,
@@ -20,6 +21,7 @@ import {
import { getIdentity } from "@/shared/api/tauriIdentity"; import { getIdentity } from "@/shared/api/tauriIdentity";
import { pubkeyToNpub } from "@/shared/lib/nostrUtils"; import { pubkeyToNpub } from "@/shared/lib/nostrUtils";
import { Button } from "@/shared/ui/button"; import { Button } from "@/shared/ui/button";
import { Input } from "@/shared/ui/input";
import { StartupWindowDragRegion } from "@/shared/ui/StartupWindowDragRegion"; import { StartupWindowDragRegion } from "@/shared/ui/StartupWindowDragRegion";
import { useSystemColorScheme } from "@/shared/theme/useSystemColorScheme"; import { useSystemColorScheme } from "@/shared/theme/useSystemColorScheme";
import { OnboardingChrome } from "@/features/onboarding/ui/OnboardingChrome"; import { OnboardingChrome } from "@/features/onboarding/ui/OnboardingChrome";
@@ -56,6 +58,8 @@ export function WelcomeSetup({
React.useState<WelcomeTransitionMode>(initialTransitionMode); React.useState<WelcomeTransitionMode>(initialTransitionMode);
const [npub, setNpub] = React.useState(""); const [npub, setNpub] = React.useState("");
const [identityError, setIdentityError] = React.useState<string | null>(null); 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 [copied, setCopied] = React.useState(false);
const communityOnboarding = useCommunityOnboarding(); const communityOnboarding = useCommunityOnboarding();
const systemColorScheme = useSystemColorScheme(); const systemColorScheme = useSystemColorScheme();
@@ -79,6 +83,24 @@ export function WelcomeSetup({
setPage(nextPage); 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( const handleInviteRedeem = React.useCallback(
(relayWsUrl: string, code: string, policyReceipt?: string) => { (relayWsUrl: string, code: string, policyReceipt?: string) => {
communityOnboarding.start({ communityOnboarding.start({
@@ -172,58 +194,106 @@ export function WelcomeSetup({
</p> </p>
</div> </div>
<div className="flex w-full flex-1 items-center justify-center pb-4 pt-12"> <div className="flex w-full flex-1 items-center justify-center pb-4 pt-12">
<div className="w-full max-w-4xl"> <div className="w-full max-w-4xl space-y-7">
<div <div>
className={ONBOARDING_KEY_FRAME_CLASS} <div
data-testid="welcome-join-npub-frame" className={ONBOARDING_KEY_FRAME_CLASS}
> data-testid="welcome-join-npub-frame"
<div className={ONBOARDING_KEY_ROW_CLASS}> >
<div className="min-w-0 flex-1"> <div className={ONBOARDING_KEY_ROW_CLASS}>
<code <div className="min-w-0 flex-1">
className={`${ONBOARDING_KEY_TEXT_CLASS} block`} <code
data-testid="welcome-join-npub" 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…"} {copied ? (
</code> <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> </div>
<Button <Button
aria-label="Copy npub" className="h-10 shrink-0 rounded-full px-6"
className="h-10 w-10 shrink-0 text-muted-foreground hover:text-foreground" disabled={!relayUrl.trim()}
disabled={!npub} type="submit"
onClick={() => {
void writeTextToClipboard(npub).then(() => {
setCopied(true);
window.setTimeout(() => setCopied(false), 1500);
});
}}
size="icon"
type="button"
variant="ghost"
> >
{copied ? ( Join community
<Check
className="h-6 w-6 text-primary"
aria-hidden="true"
/>
) : (
<Copy className="h-6 w-6" aria-hidden="true" />
)}
</Button> </Button>
</div> </div>
</div> </form>
{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>
)}
</div> </div>
</div> </div>
<OnboardingFooter> <OnboardingFooter>
@@ -190,7 +190,11 @@ export function CommunityOnboardingFlow({
}; };
}, [clear, isEnteringStage]); }, [clear, isEnteringStage]);
const retryClaim = () => update({ stage: "claiming", error: undefined }); const retry = () =>
update({
stage: transaction?.inviteCode ? "claiming" : "connecting",
error: undefined,
});
const relayUrl = transaction?.relayUrl; const relayUrl = transaction?.relayUrl;
const finish = React.useCallback(async () => { const finish = React.useCallback(async () => {
if (!relayUrl) return; if (!relayUrl) return;
@@ -362,7 +366,7 @@ export function CommunityOnboardingFlow({
</p> </p>
<div className="mt-6 flex justify-center gap-3"> <div className="mt-6 flex justify-center gap-3">
{transaction.error ? ( {transaction.error ? (
<Button className="rounded-full px-6" onClick={retryClaim}> <Button className="rounded-full px-6" onClick={retry}>
Retry Retry
</Button> </Button>
) : null} ) : null}