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 { 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}
|
||||||
|
|||||||
Reference in New Issue
Block a user