mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
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:
+49
-3
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user