fix: JoinPage graceful already-member handling + "I use another instance" for auth'd users
- Add spaceId to InvitePreview so the client can navigate to the space - Detect "already a member" error and show green success card with auto-redirect - Add "I use another instance" link for authenticated users alongside "Not you? Log in"
This commit is contained in:
@@ -1103,6 +1103,7 @@ export async function spaceRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
const instanceName = settings?.instanceName ?? 'Backspace';
|
const instanceName = settings?.instanceName ?? 'Backspace';
|
||||||
|
|
||||||
return reply.code(200).send({
|
return reply.code(200).send({
|
||||||
|
spaceId: space.id,
|
||||||
spaceName: space.name,
|
spaceName: space.name,
|
||||||
description: space.description ?? null,
|
description: space.description ?? null,
|
||||||
icon: space.icon ?? null,
|
icon: space.icon ?? null,
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export interface Space {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface InvitePreview {
|
export interface InvitePreview {
|
||||||
|
spaceId: string;
|
||||||
spaceName: string;
|
spaceName: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
icon: string | null;
|
icon: string | null;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { parseInviteInput } from '../utils/inviteParser';
|
|||||||
import { Avatar } from './ui/Avatar';
|
import { Avatar } from './ui/Avatar';
|
||||||
import type { InvitePreview } from '@backspace/shared';
|
import type { InvitePreview } from '@backspace/shared';
|
||||||
|
|
||||||
type JoinPhase = 'preview' | 'connect' | 'fallback' | 'other-instance';
|
type JoinPhase = 'preview' | 'connect' | 'fallback' | 'other-instance' | 'already-member';
|
||||||
|
|
||||||
export function JoinPage() {
|
export function JoinPage() {
|
||||||
const { inviteCode: rawInviteCode } = useParams<{ inviteCode: string }>();
|
const { inviteCode: rawInviteCode } = useParams<{ inviteCode: string }>();
|
||||||
@@ -106,7 +106,13 @@ export function JoinPage() {
|
|||||||
setPhase('connect');
|
setPhase('connect');
|
||||||
setError('');
|
setError('');
|
||||||
} else {
|
} else {
|
||||||
setError(err instanceof Error ? err.message : 'Failed to join space');
|
const msg = err instanceof Error ? err.message : 'Failed to join space';
|
||||||
|
if (msg.toLowerCase().includes('already a member')) {
|
||||||
|
setPhase('already-member');
|
||||||
|
setError('');
|
||||||
|
} else {
|
||||||
|
setError(msg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsJoining(false);
|
setIsJoining(false);
|
||||||
@@ -130,7 +136,13 @@ export function JoinPage() {
|
|||||||
setFallbackPassword('');
|
setFallbackPassword('');
|
||||||
setError('');
|
setError('');
|
||||||
} else {
|
} else {
|
||||||
setError(err instanceof Error ? err.message : 'Failed to connect');
|
const msg = err instanceof Error ? err.message : 'Failed to connect';
|
||||||
|
if (msg.toLowerCase().includes('already a member')) {
|
||||||
|
setPhase('already-member');
|
||||||
|
setError('');
|
||||||
|
} else {
|
||||||
|
setError(msg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsJoining(false);
|
setIsJoining(false);
|
||||||
@@ -148,7 +160,13 @@ export function JoinPage() {
|
|||||||
const space = await joinByCode(parsed.code, parsed.origin);
|
const space = await joinByCode(parsed.code, parsed.origin);
|
||||||
navigate(`/channels/${space.id}`);
|
navigate(`/channels/${space.id}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Failed to log in');
|
const msg = err instanceof Error ? err.message : 'Failed to log in';
|
||||||
|
if (msg.toLowerCase().includes('already a member')) {
|
||||||
|
setPhase('already-member');
|
||||||
|
setError('');
|
||||||
|
} else {
|
||||||
|
setError(msg);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsJoining(false);
|
setIsJoining(false);
|
||||||
}
|
}
|
||||||
@@ -295,6 +313,14 @@ export function JoinPage() {
|
|||||||
>
|
>
|
||||||
Log in
|
Log in
|
||||||
</button>
|
</button>
|
||||||
|
{' · '}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => { setPhase('other-instance'); setError(''); }}
|
||||||
|
className="text-accent-primary hover:underline"
|
||||||
|
>
|
||||||
|
I use another instance
|
||||||
|
</button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : token ? (
|
) : token ? (
|
||||||
@@ -431,6 +457,11 @@ export function JoinPage() {
|
|||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Phase: already-member — green success with auto-redirect */}
|
||||||
|
{phase === 'already-member' && preview && (
|
||||||
|
<AlreadyMemberCard spaceName={preview.spaceName} spaceId={preview.spaceId} navigate={navigate} />
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Phase: fallback — different password on remote */}
|
{/* Phase: fallback — different password on remote */}
|
||||||
{phase === 'fallback' && (
|
{phase === 'fallback' && (
|
||||||
<form onSubmit={handleFallbackLogin}>
|
<form onSubmit={handleFallbackLogin}>
|
||||||
@@ -484,3 +515,28 @@ export function JoinPage() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function AlreadyMemberCard({ spaceName, spaceId, navigate }: { spaceName: string; spaceId: string; navigate: (path: string) => void }) {
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => navigate(`/channels/${spaceId}`), 2000);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [spaceId, navigate]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="w-12 h-12 mx-auto mb-3 rounded-full bg-accent-mint/10 flex items-center justify-center">
|
||||||
|
<svg className="w-6 h-6 text-accent-mint" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p className="text-txt-primary font-medium mb-1">You're already in {spaceName}!</p>
|
||||||
|
<p className="text-txt-tertiary text-xs mb-4">Redirecting you now...</p>
|
||||||
|
<button
|
||||||
|
onClick={() => navigate(`/channels/${spaceId}`)}
|
||||||
|
className="px-6 py-2.5 bg-accent-primary hover:bg-accent-primary/80 text-white font-medium rounded transition-colors"
|
||||||
|
>
|
||||||
|
Go to {spaceName}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user