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:
Jannis Braun
2026-03-13 03:58:28 +01:00
parent a3a045905e
commit fed2a64d3f
3 changed files with 62 additions and 4 deletions
+1
View File
@@ -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,
+1
View File
@@ -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;
+60 -4
View File
@@ -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>
);
}