Files
DiscordClone/packages/shared/src/components/auth/InviteAcceptPage.tsx
Bryan1029384756 b7a4cf4ce8
All checks were successful
Build and Release / build-and-release (push) Successful in 13m12s
feat(ui): add Button, Modal, Spinner, Toast, and Tooltip components with styles
- Implemented Button component with various props for customization.
- Created Modal component with header, content, and footer subcomponents.
- Added Spinner component for loading indicators.
- Developed Toast component for displaying notifications.
- Introduced Tooltip component for contextual hints with keyboard shortcuts.
- Added corresponding CSS modules for styling each component.
- Updated index file to export new components.
- Configured TypeScript settings for the UI package.
2026-04-14 09:02:14 -05:00

137 lines
3.7 KiB
TypeScript

/**
* InviteAcceptPage — handles `/invite/:code#key=<secret>`.
*
* Pulls the encrypted payload from `api.invites.use`, decrypts it
* with the URL-fragment secret, stashes the decoded channel-key
* map in sessionStorage under `pendingInviteKeys` / `pendingInviteCode`,
* then forwards the user to the register page (or to `/channels/@me`
* if they're already logged in).
*
* The fragment secret never hits the server — browsers don't send
* `#…` to origin servers — so knowledge of it is the capability that
* unlocks the invite payload.
*/
import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useConvex } from 'convex/react';
import { api } from '../../../../../convex/_generated/api';
import { usePlatform } from '../../platform';
import { AuthLayout } from './AuthLayout';
export function InviteAcceptPage() {
const { code } = useParams<{ code: string }>();
const navigate = useNavigate();
const convex = useConvex();
const { crypto } = usePlatform();
const [status, setStatus] = useState<'working' | 'error'>('working');
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
const run = async () => {
if (!code) {
setStatus('error');
setError('Missing invite code.');
return;
}
const hash =
typeof window !== 'undefined' ? window.location.hash : '';
// Fragment looks like "#key=abcd…"
const match = hash.match(/[#&]key=([^&]+)/);
const secret = match ? match[1] : null;
if (!secret) {
setStatus('error');
setError(
'Invite link is missing its secret. Ask the sender for a fresh link.',
);
return;
}
try {
const result = await convex.query(api.invites.use, { code });
if (cancelled) return;
if ('error' in result) {
setStatus('error');
setError(result.error);
return;
}
// Older invites used to send a placeholder empty payload.
// Treat that as "no channel keys to import" and continue
// so the user can still register with the code.
let keys: Record<string, string> = {};
if (result.encryptedPayload) {
try {
const blob = JSON.parse(result.encryptedPayload) as {
c: string;
i: string;
t: string;
};
const plaintext = await crypto.decryptData(
blob.c,
secret,
blob.i,
blob.t,
);
keys = JSON.parse(plaintext) as Record<string, string>;
} catch {
setStatus('error');
setError(
'Failed to decrypt invite payload. The secret may be wrong or the invite corrupted.',
);
return;
}
}
try {
sessionStorage.setItem('pendingInviteCode', code);
sessionStorage.setItem('pendingInviteKeys', JSON.stringify(keys));
} catch {
/* storage blocked — flow still works if same tab */
}
// If the user is already signed in, just take them home —
// the invite keys are already theirs in that case.
const loggedIn = !!sessionStorage.getItem('privateKey');
if (loggedIn) {
navigate('/channels/@me', { replace: true });
} else {
navigate('/register', { replace: true });
}
} catch (err: any) {
if (cancelled) return;
setStatus('error');
setError(err?.message ?? 'Failed to accept invite.');
}
};
void run();
return () => {
cancelled = true;
};
}, [code, convex, crypto, navigate]);
return (
<AuthLayout>
<h1 style={{ color: 'var(--text-primary)' }}>
{status === 'working' ? 'Accepting invite…' : 'Invite Error'}
</h1>
{error && (
<p
style={{
color: 'var(--status-danger, #ed4245)',
marginTop: 12,
fontSize: 14,
}}
>
{error}
</p>
)}
</AuthLayout>
);
}
export default InviteAcceptPage;